分布式ID生成方案

2022-10-30,,,

系统唯一ID是设计一个系统的时候常常会遇到的问题,也常常为这个问题而纠结。

生成ID的方法有很多,适应不同的场景、需求以及性能要求。所以有些比较复杂的系统会有多个ID生成的策略。

0. 分布式ID要求

(1)全局唯一性:不能出现重复的ID号,既然是唯一标识,这是最基本的要求;

(2)粗略有序:如果在分布式环境中做到完全有序,需要用到锁等,考虑到性能,采用粗略有序,具体分为秒级有序和毫秒级有序;

(3)可反解:即生成ID服务提供反解方法,这样在存储时就能以十进制存储,省下传统timestamp类字段的占用空间了;

(4)可伸缩:中心发布模式时可以进行集群部署,这样在生成ID里就必须包含机器ID;

(5)趋势递增:在MySQL InnoDB引擎中使用的是聚集索引,由于多数RDBMS使用B-Tree的数据结构来存储索引数据,在主键的选择上我们应该尽量使用有序的主键保证写入性能;

(6)单调递增:保证下一个ID一定大于上一个ID,例如事务版本号,IM增量信息,排序特殊需求;

(7)信息安全:如果ID是连续的,恶意用户的扒取工作就非常容易,直接按照顺序下载指定URL即可,如果是订单号就更危险了

1. 数据库自增长列或字段

针对主库单点,如果有个多个master库,则每个master库设置的起始数字不一样,步长一样(可以使master的个数)。比如:master1生成的是1,4,7,10,master2生成的是2,5,8,11,master3生成的是3,6,9,12。这样就可以有效生成集群中的唯一ID,也可以大大降低ID生成数据库操作的负载。

优点:

(1)最常见的方式,利用数据库,全数据库唯一,简单,代码方便,性能可以接受

(2)数字ID天然排序,对分页或需要排序的结果很有帮助

缺点:

(1)不同数据库语法和实现不同,数据库迁移的时候或多数据库版本支持的时候需要处理

(2)在单个数据库或读写分离或一主多从的情况下,只有一个主库可以生成,有单点故障的风险

(3)在性能达不到要求的情况下,比较难于扩展

(4)如果遇到多个系统需要合并或者涉及到数据迁移会相当痛苦

(5)分表分库的时候有麻烦

2. UUID

优点:

(1)常见的方式,可以利用数据库也可以利用程序生成,一般来说全球唯一,简单,代码方便

(2)生成ID性能非常好,基本不会有性能问题

(3)全球唯一,在遇见数据迁移,系统数据合并或数据库变更等情况下,可以从容应对

缺点:

(1)没有排序,无法保证趋势递增

(2)UUID往往是使用字符串存储,查询的效率比较低

(3)存储空间比较大,如果是海量数据库,就需要存储量的问题

(4)传输数据量大

(5)不可读

3. Redis生成ID

当使用数据库来生成ID性能不够要求的时候,可以尝试用Redis来生成ID。

4. Twitter的snowflake算法

snowflake是Twitter开源的分布式ID生成算法,结果是一个long型的ID。其核心思想是:

使用41bit作为毫秒数

10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID)

12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生4096个ID)

最后还有一个符号位,永远是0。

https://github.com/twitter/snowflake

优点:

(1)不依赖数据库,灵活方便,且性能优于数据库

(2)ID按照时间在单机上是递增的

缺点:

(1)在单机上是递增的,但是由于涉及到分布式环境中,每台机器上的时钟不可能完全同步,也许有时候也会出现不是全局递增的情况。

5. 利用zookeeper生成唯一ID

zookeeper主要通过其znode数据版本来生成序列号,可以生成32位和64位的数据版本号,客户端可以使用这个版本号来作为唯一的序列号。

很少会使用zookeeper来生成唯一ID,主要是由于依赖zookeeper,并且是多步调用API,如果在竞争较大的情况下,需要考虑使用分布式锁。因此,性能在高并发的分布式环境中,也不甚理想。

6. MongoDB的ObjectId

MongoDB的ObjectId和snowflake算法类似。它涉及成轻量级,不同的机器都能用全局唯一的同种方法方便地生成它。MongoDB从一开始就设计用来作为分布式数据库,处理多个节点是一个核心要求,使其在分布式环境中要容易生成得多。

前4个字节是从标准纪元开始的时间戳,单位是秒。时间戳和随后的5个字节组合起来,提供了秒级别的唯一性。由于时间戳在前,意味着ObjectId大致会按照插入的顺序排列。这对于某些方面很有用,如将其作为索引提高效率。这4个字节也隐含了文档创建时间。绝大多数客户端类库都会公开一个方法从ObjectId获取这个信息。

接下来3字节是所在主机的唯一标识符。通常是主机名的散列值,这样就可以确保不同主机生成不同的ObjectId,不产生冲突。

接下来2个字节来自产生ObjectId的进程标识符(PID),为了确保在同一台机器上并发的多个进程产生的ObjectId是唯一的。

前9字节保证了同一秒中不同机器不同进程产生的ObjectId是唯一的,后3字节就是一个自动增加的计数器,确保相同进程同一秒产生的ObjectId也是不一样的。同一秒钟最多允许每个进程拥有16777216个不同的ObjectId。

7. 单点批量ID生成服务

分布式系统之所以难,很重要的原因之一是“没有一个全局时钟,难以保证绝对的时序”,要想保证绝对的时序,还是只能使用单点服务,用本地时钟保证“绝对时序”。数据库写压力大,是因为每次生成ID都访问了数据库,可以使用批量的方式降低数据库写压力。

数据库中只存储当前ID的最大值,例如0。ID生成服务假设每次批量拉取6个ID,服务访问数据库,将当前ID的最大值修改为5,这样应用访问ID生成服务索要ID,ID生成服务不需要每次访问数据库,就能依次派发0,1,2,3,4,5这些ID了,当ID发完后,再将ID的最大值修改为11,就能再次派发6,7,8,9,10,11这些ID了,于是数据库的压力就降低到原来的1/6了。

优点

(1)保证了ID生成的绝对递增有序

(2)大大的降低了数据库的压力,ID生成可以做到每秒生成几万几十万个

缺点

(1)服务仍然是单点

(2)如果服务挂了,服务重启起来之后,继续生成ID可能会不连续,中间出现空洞(服务内存是保存着0,1,2,3,4,5,数据库中max-id是5,分配到3时,服务重启了,下次会从6开始分配,4和5就成了空洞,不过这个问题也不大)

(3)虽然每秒可以生成几万几十万个ID,但毕竟还是有性能上限,无法进行水平扩展

改进方法

单点服务的常用高可用优化方案是“备用服务”,也叫“影子服务”,所以我们能用以下方法优化上述缺点(1)。对外提供的服务是主服务,有一个影子服务时刻处于备用状态,当主服务挂了的时候影子服务顶上。这个切换的过程对调用方是透明的,可以自动完成,常用的技术是vip+keepalived,具体就不在这里展开。

参考资料:

https://www.cnblogs.com/haoxinyue/p/5208136.html

https://tech.meituan.com/MT_Leaf.html

分布式ID生成方案的相关教程结束。

《分布式ID生成方案.doc》

下载本文的Word格式文档,以方便收藏与打印。