
将程序视点设为星标精品文章第一时间阅读
大家好,欢迎来到程序视点!我是小二哥。
在业务开发中,大量场景需要唯一ID来进行标识:用户需要唯一身份标识、商品需要唯一标识、消息需要唯一标识、事件需要唯一标识等,都需要全局唯一ID,尤其是复杂的分布式业务场景中全局唯一ID更为重要。于是就会引申出分布式系统中唯一主键ID生成策略问题。

最贴近我们开发者的场景就是:大数据量下,一张数据库表无法满足性能和扩展时,就会对其进行分库分表。涉及到分库分表,就不得不考虑分布式唯一 ID 生成方案啦!
对于分布式ID而言,也需要具备分布式系统的特点:高并发,高可用,高性能等特点。那么,分布式唯一ID有哪些特性或要求呢?

分布式ID最低要求
优秀的分布式 ID
根据上文提到的特征,小二哥整理了最常用的分布式 ID 解决方案,大体可以分三大类方案:数据库方案、算法方案、开源组件方案。
下面我们一一进行分享。
核心思想是使用数据库的id自增策略。给到大家这个提示,应该脑海里应该已经知道怎么回事了~我们还是直接来看它的优缺点吧~
优点: ① 简单,天然有序。 ② 数值类型查询速度快。
缺点: ① DB单点存在宕机风险。 ② 并发性不好,无法扛住高并发场景。 ③ 数据库写压力大。 ④ 存在数量泄露风险。
这种方式的缺点非常多,尤其是访问量激增时MySQL本身就是系统的瓶颈,用它来实现分布式服务风险比较大,不推荐!
刚刚说了单点数据库方式不可取,那对上边的方式做一些高可用优化,换成主从模式集群。
但主从模式只解决了单点问题。另外,主节点仍旧有挂掉的风险。因此,我们自然想到双主模式集群,也就是两个Mysql实例都能单独的生产自增ID。
那这样还会有个问题,两个MySQL实例的自增ID都从1开始,会生成重复的ID怎么办?
核心思想是,将数据库进行水平拆分,每个数据库设置不同的初始值和相同的自增步长。
MySQL 1配置:

MySQL 2 配置:

这样两个MySQL实例的自增ID分别就是:
1、3、5、7、9
2、4、6、8、10
这个方案很不错,但仍旧有个问题:如果集群后的性能还是扛不住高并发咋办?就要进行MySQL扩容增加节点,这是一个比较麻烦的事。再增加一个MySQL 3 配置,初始值和步长设置多少呢? 这时你就要考虑ID是否出现重复的问题了。如果你想到你的应用需要3台MySQL来生成ID,你的设计应该是这样才可以:

但可能你的业务快速增长,3台MySQL都不够用了。再扩容就又出现无ID初始值可分的窘境。因此,这种方案的优缺点非常明显。
优点:
解决DB单点问题
缺点:
不利于后续扩容,而且实际上单个数据库自身压力还是大,依旧无法满足高并发场景。
但这并不影响该方案在现实场景中的使用。至于该方案的缺点,是有相应解决方案的: ① 根据扩容考虑决定步长。 ② 增加其他位标记区分扩容。 这其实都是在需求与方案间的权衡,根据需求来选择最适合的方式。
号段模式是当下分布式ID生成器的主流实现方式之一了。
无论是数据库自增ID,还是数据库集群模式,每次获取 ID 都要访问一次数据库,数据库压力大。因此,可以批量获取一批ID,然后存在内存里面,需要用到的时候,直接从内存里面拿来使用。
核心思想是使用单台数据库批量的获取自增ID,再分给不同的机器去消费。这样数据库的压力也会减小到N分之一,且故障后可坚持一段时间,同时避免固定步长带来的扩容问题。

这种模式一举多得,性能、安全、扩展都比之前的都要好。但这种做法的缺点是服务器重启、单点故障会造成ID不连续。还是那句话,没有最好的方案,只有最适合的方案。
Redis也同样可以实现。核心思想是Redis的所有命令操作都是单线程的,本身提供像 incr 和 increby 这样的自增原子命令,所以能保证生成的 ID 肯定是唯一有序的。这种方法是线程安全的,可以在分布式系统中使用。
优点: ① 不依赖于数据库,灵活方便,且性能优于数据库。② 数字ID天然排序,对分页或者需要排序的结果很有帮助。
缺点: ① 如果系统中没有Redis,还需要引入新的组件,增加系统复杂度。② 需要编码和配置的工作量比较大。③ 生成的 ID 是有序递增的,存在数据量泄露。
另外,使用redis实现需要注意一点:要考虑到redis持久化的问题。redis有两种持久化方式RDB和AOF。
照理说,Redis单机号称10w+的能力,一般是没有问题的。但考虑到单节点的性能瓶颈,我们可以利用前面MySQL数据库乐视的优化方案。可以使用 Redis 集群来获取更高的吞吐量(①数据库水平拆分,设置不同的初始值和相同的步长;②批量缓存自增ID)。
还有一种生成分布式ID的方案就是MongoDB ObjectId。核心思想是使用12字节(24bit)的BSON 类型字符串作为ID,并将所占的24bit 划分成多段。

MongoDB ObjectId是MongoDB数据库中的一个内置数据类型,用于唯一标识MongoDB文档(Document),由12个字节组成,其中前4个字节表示时间戳,接下来3个字节表示机器ID,然后2个字节表示进程ID,最后3个字节表示随机值。
优点: ① 本地生成,没有网络消耗,生成简单,没有高可用风险。 ② 所生成的ID包含时间信息,可以提取时间信息。
缺点: ① 不易于存储:12字节24位长度的字符串表示,很多场景不适用。 ② 当机器时间不对的情况下,可能导致会产生重复 ID。
因此,MongoDB ObjectID方案的选用还是要注意业务场景需求,不能盲目选用。
谈到具有唯一性的ID,首先被想到可能就是UUID了,毕竟它有着全球唯一的特性。那么UUID可以做分布式ID吗?答案是可以的,但是并不推荐。
核心思想是结合机器的网卡(基于名字空间/名字的散列值MD5/SHA1)、当地时间(基于时间戳&时钟序列)、一个随记数来生成UUID。
UUID的生成简单到爆,只需一行代码就能输出结果,但却并不适用于实际的业务需求。
优点: ① 本地生成,没有网络消耗,生成简单,没有高可用风险。
缺点: ① 不易于存储:UUID太长,16字节128位,通常以36长度的字符串表示,很多场景不适用。 ② 信息不安全:基于MAC地址生成UUID的算法可能会造成MAC地址泄露,这个漏洞曾被用于寻找梅丽莎病毒的制作者位置。 ③ 无序查询效率低:由于生成的UUID是无序不可读的字符串,所以其查询效率低。④ 当机器时间不对的情况下,可能导致会产生重复 ID。
由于不推荐使用这种方式,关于UUID更多的细节这里就不展开啦
介绍的分布式ID方案,大名鼎鼎的 snowflake算法 就不得不说。核心思想是把64-bit分别划分成多段,分开来标示机器、时间、某一并发序列等,从而使每台机器及同一机器生成的ID都是互不相同。
雪花算法是 Twitter 提出的一种分布式ID生成算法。雪花算法可以在多台机器上生成不重复的ID,支持高并发和大规模的分布式系统,但需要保证数据中心ID和机器ID的唯一性。
PS:实际上这种算法使用可以很灵活,根据自身业务的并发情况、机器分布、使用年限等,可以自由地重新决定各部分的位数,从而增加或减少某部分的量级。在该算法影响下各大公司相继开发出各具特色的分布式生成器。我们即将提到的开源组件方案中,都是基于雪花算法做的影子。
细说下 snowflake算法下, ID组成结构,生成的64位ID可以分成5个部分:
1位符号位标识 - 41位时间戳 - 5位数据中心标识 - 5位机器标识 - 12位序列号


默认情况下,41bit的时间戳可以支持该算法使用到2082年,10bit的工作机器id可以支持1024台机器,序列号支持1毫秒产生4096个自增序列id。再来看看它的优缺点。
优点: ①整体上按照时间按时间趋势递增,后续插入索引树的时候性能较好。 ②整个分布式系统内不会产生ID碰撞(由数据中心标识ID、机器标识ID作区分)。 ③本地生成,且不依赖数据库(或第三方组件),没有网络消耗,所以效率高(经测试,每秒能够产生26万ID左右)。
缺点: ①由于雪花算法是强依赖于时间的,在分布式环境下,如果发生时钟回拨,很可能会引起ID重复、ID乱序、服务会处于不可用状态等问题。
对于获得一致好评的算法,它的缺点是有相应解决方案的:a. 将ID生成交给少量服务器,并关闭时钟同步。 b. 直接报错,交给上层业务处理。 c. 如果回拨时间较短,在耗时要求内,比如5ms,那么等待回拨时长后再进行生成。 d. 如果回拨时间很长,那么无法等待,可以匀出少量位(1~2位)作为回拨位,一旦时钟回拨,将回拨位加1,可得到不一样的ID,2位回拨位允许标记3次时钟回拨,基本够使用。如果超出了,可以再选择抛出异常。
之前说个,雪花算法是非常灵活的,毕竟不是每家公司的每个业务都能把算法中的数量级拉满!因此,就有相应的改进后的开源组件。这里分享几个给大家。
8. uid- generator(百度)
uid-generator是百度开源的一款基于 Snowflake的唯一 ID 生成器,是对 Snowflake进行了改进。
uid-generator与原始的snowflake算法不同在于,uid-generator支持自定义时间戳、工作机器ID和 序列号 等各部分的位数,而且uid-generator中采用用户自定义workId的生成策略。
另外uid-generator需要与数据库配合使用,需要新增一个WORKER_NODE表。当应用启动时会向数据库表中去插入一条数据,插入成功后返回的自增ID就是该机器的workId数据由host,port组成。
uid-generator ID组成结构:

workId,占用了22个bit位,时间占用了28个bit位,序列化占用了13个bit位,每秒可支持8192的并发,已经很不错了。还需要注意的是,和原始的snowflake不太一样,时间的单位是秒,而不是毫秒,workId也不一样,而且同一应用每次重启就会消费一个workId。更多内容,请参考如下地址。
GitHub地址 https://github.com/baidu/uid-generator
Tinyid是滴滴开源的一款
基于数据库号段模式的唯一 ID 生成器。

前面讲过基于数据库的号段模式了。这里就简单说说Tinyid的一些特别之处。性能
① http方式访问,性能取决于http server的能力,网络传输速度。
② java-client方式,id为本地生成,号段长度(step)越长,qps越大,如果将号段设置足够大,则qps可达1000w+。
可用性
① 依赖db,当db不可用时,因为server有缓存,所以还可以使用一段时间,如果配置了多个db,则只要有1个db存活,则服务可用。
② 使用tiny-client,只要server有一台存活,则理论上可用,server全挂,因为client有缓存,也可以继续使用一段时间。
Tinyid的特性
适用场景:只关心id是数字,趋势递增的系统,可以容忍id不连续,有浪费的场景。
不适用场景:类似订单id的业务(因为生成的id大部分是连续的,容易被扫库、或者测算出订单量)
具体使用请查看官方说明。
GitHub地址 https://github.com/didi/tinyid
Leaf 是美团开源的分布式ID生成器,能保证全局唯一性、趋势递增、单调递增、信息安全,里面也提到了几种分布式方案的对比,但也需要依赖关系数据库、Zookeeper等中间件。
Leaf提供了号段模式 和 Snowflake这两种模式来生成分布式 ID。号段模式依赖数据库表,Snowflake依赖于ZooKeeper。
关于Leaf的更多内容,请查看下方地址。
GitHub地址 https://github.com/meituan-diaNPing/leaf
以上是一些常见的分布式 ID 生成方式,旨在给大家一个详细学习的方向,每种生成方式都有它自己的优缺点,具体选择应该根据实际需求和场景来决定。
大家还接触过哪些其他的分布式ID方案呢?欢迎大家留言讨论哦~