max_id:当前最大可用 ID。
step:号段的步长。
bit_type:业务类型。
version:记录更新的版本号,主要作用是乐观锁,每次更新时都会更新该值,以保证并发时数据的正确性。
每次从数据库中获取号段 ID 的范围时,都会执行更新语句,其中计算新号段范围最大值 max_id 的公式是 max_id + step 组成,所以 SQL 中设置 max_id = max_id+step 来执行更新语句,更新数据库中这个范围最大值 max_id,然后再通过查询语句查询更新后 ID 最大值,再根据最大值 max_id 与步长 step 计算出待生成的 ID 的范围,其中操作的 SQL 如下:
1begin
2UPDATE `myid` SET `max_id` = max_id + step, `version` = version + 1 WHERE `version` = {执行更新的版本号} AND `biz_type` = {业务类型}
3SELECT `max_id`, `step`, `version` FROM `myid` WHERE `biz_type` = {业务类型}
4commit
其中数据库表中内容大同小异,如下是某个测试数据的样式:
max_id
biz_type
version
数据库号段模式生成 ID 过程描述
例如,某个业务需要批量获取 ID,首先它往数据库 myid 中插入一条初始化值,设置 max_id=0 和步长 step=1000 及使用该 ID 的业务标识 biz_type=test 与版本 version=0,如下:
1
INSERT INTO `myid`(`max_id`,`step`,`biz_type`,`version`) VALUES(0,1000,"test",0)
然后可以观察到数据库中多了一条数据:
max_id
biz_type
version
然后执行获取分布式 ID 的方法,方法中应执行下面语句进行号段更新,方便生成新的一批号段:
1begin
2UPDATE `myid` SET `max_id` = max_id + step, `version` = version + 1 WHERE `version` = 0 AND `biz_type` = test
3SELECT `max_id`, `step`, `version` FROM `myid` WHERE `biz_type` = test
4commit
这时候数据库中的值为:
max_id
biz_type
version
缓存大小:1000
每次都从缓存中取值,创建一个监听器用于监听缓存中 ID 消耗比例,设置阈值,判断如果取值超过的阈值后就进行数据库号段更新操作,跟上面第一次执行更新时候一样,也是执行下面的更新 SQL 语句:
1begin
2UPDATE `myid` SET `max_id` = max_id + step, `version` = version + 1 WHERE `version` = 0 AND `biz_type` = test
3SELECT `max_id`, `step`, `version` FROM `myid` WHERE `biz_type` = test
4commit
比如,设置阈值为 50%,当缓存中存在 1000 个 ID,监听器监听到业务应用已经消耗到 500 个后(超过阈值),创建一个新的线程去执行上面的更新 SQL 语句,让数据库中号段范围按照设置的 step 扩大,然后获取新的号段最大值,应用中再生成一批范围为 (1001,2000] 范围的 ID 存入缓存供应用使用,这时候缓存中数据大小为:
缓存大小:2000(已经使用 500,可用 1500)
过程是个循环的过程,每到消耗到一定数据后就会生成新的一批。这里只是对其进行了简单介绍,很多时候为了保证数据库可用性都会采用集群模式,现在通过号码模式生成 ID 的开源框架有很多,比如:
美团开源的 Leaf
滴滴开源的 TinyId
数据库号段模式生成 ID 的优缺点
趋势递增;
使用缓存机制,容灾性高,即使数据库不可用还能撑一段时间。
可以自定义每次扩展的大小,控制 ID 生成速度;
可以设置生成 ID 的初始范围,方便业务从原有的 ID 方式上迁移过来。
数据库宕机会造成整个系统不可用。
ID 号码不够随机,可能够泄露发号数量的信息,不太安全。
所以,采用这种方案我们也经常使用数据库多主模式,保证数据库的高可用性。
方案五:使用 Redis 单节点实现分布式 ID
Redis 中实现分布式 ID 方法
Redis 中存在原子操作指令 INCR 或 INCRBY,执行后可用于创建初始化值或者在原有数字基础上增加指定数字,并返回执行 INCR 命令之后 key 的值,这样就可以很方便的创建有序递增的 ID。
INCR: 将 key 中储存的数字值增一,如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作。
INCRBY: 将 key 中储存的数字加上指定的增量值,如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作。
获取到有序递增的值后,我们会对该值进行"时间戳"+"随机值"+"Redis递增编号"处理,组合方式多种多样,这里说的只是其中一种方式。
Java 中操作 Redis 生成 ID 示例
在 Java 中可以使用 Jedis 组件操作 Redis,可以 Maven 中引入 Jedis 相关包:
1<dependency>
2 <groupId>redis.clients</groupId>
3 <artifactId>jedis</artifactId>
4 <version>3.3.0</version>
5</dependency>
然后使用 INCR 命令生成 ID:
1public class RedisTest {
3 public static void main(String[] args) {
4 // 创建 Jedis 客户端
5 Jedis jedis = new Jedis("127.0.0.1", 6379);
6 // 执行 INCR 并获取 ID 值
7 long id = jedis.incr("myid");
8 // 输出 ID 值(正常情况下会执行'时间戳'+'随机值'+'Redis ID'进行组合使用)
9 System.out.println(id);
10 // 关闭连接
11 jedis.close();
12 }
这里只是简单介绍如何获取 INCR 命令自增的 ID 值,拿到 ID 后如何与时间戳、随机值等拼接组合,这点还需要考虑。
Redis 单节点实现分布式 ID 的优缺点
实现简单;
有序递增,方便排序;
强依赖于 Redis,可能存在单点问题;
如果 Redis 超时,可能会对业务造成影响;
占用宽带,而且需要考虑网络延时等问题带来地性能冲击。
方案六:使用 Redis 集群实现分布式 ID
为什么使用 Redis 集群模式
使用 Redis 单机生成 ID 存在性能瓶颈,无法满足高并发的业务需求,且一旦 Redis 崩溃或者服务器宕机,那么将导致整个基于它的服务不可用,这是业务中难以忍受的,所以一般时候会用集群的方式来实现 Redis 的分布式 ID 方案。
Redis 集群模式下如何实现分布式 ID
使用集群的方式需要设置提前设置 初始值 和 步长 来保证每个节点增加的 ID 不会冲突,正常做法每个节点都配置一个跟节点挂钩的 Lua 脚本,脚本内容中设置好对应节点的 初始值 和 步长,其中初始值是按照节点个数从 1 开始递增分配,而步长则是等于集群中 Master 节点的个数。按照这种方式生成 ID 并获取后,后面的执行逻辑跟单节点 Redis 一样,都是对 ID 进行加工处理操作。
假如 Redis 集群中存在 3 台 Master 节点,那么就可以针对每个节点的 Lua 脚本中配置初始值和步长,如下:
然后应用中获取到该 ID 值后与时间戳、随机值等组合处理,生成一个唯一 ID。
Redis 集群模式下生成分布式 ID 的优缺点
Redis 集群方案和单机比,ID 的有序递增变为趋势递增,且集群模式保证了可用性。
集群模式,高可用;
趋势递增,方便分类、排序;
如果 Redis 超时,可能会对业务造成影响;
存在网络开销,集群模式需要数据同步,对性能有影响;
集群规模固定后,改动规则影响很大,所以扩展比较困难;
方案七:利用 Snowflake 算法实现 ID 生成
Snowflake 算法是什么
Snowflake 算法是 Twitter 开源的分布式 ID 生成算法,我们常称其为雪花算法。其生产的结果 ID 是一个 64bit 的 ID,能够很方便的使用 long 类型进行存储。其核心组成是使用 41bit 作为毫秒数,10bit 作为机器的 ID(5个bit是数据中心,5个bit的机器ID),12bit 作为毫秒内的序列号。
Snowflake 算法的组成
其结构组成:
1 位的正负标识位: 由于 long 基本类型在 Java 中是带符号的,整数为 0 负数为 1,一般生成的 ID 都为正数,所以固定为0;
41 位的时间戳: 该时间戳为毫秒级,时间戳不是存储当前时间的时间戳,而是存储时间的差值(当前时间-固定的开始时间),这里的的开始时间戳为我们的ID生成器开始使用的时间,通过计算(1L << 41) / (1000L * 60 * 60 * 24 * 365)得出69,说明该算法生成的 ID 足够我们使用69年。
10 位的 WorkerID: 一般是5位数据中心标识与 5 位机器标识共同组成,以区分不同的集群节点,相当于能在1024个节点上生成 ID。
12 位的序列号: 在同一机器同一毫秒内可生成不同的序列号,12位支持最多能生成4096个。
雪花算法效率很高,理论上其生成 ID 的 QPS 约为 409.6w/s,这种分配方式可以保证在任何一个机房的任何一台机器在任意毫秒内生成的 ID 都是不同的。
Snowflake 算法的扩展位
在实际使用过程中,我们往往都会根据具体的业务对雪花算法的组成进行改动,常改动的是10bit的 WorkerID 位置,该位置由5位数据中心标识与5位机器标识共同组成,那么这时候可以:
如果部署的服务都在同一个数据中心,即不考虑数据中心概念,可以将5bit数据中心为替换成我们的业务编码。
如果数据中心不是很多,这时候可以将5bit数据中心位拆成3bit+2bit,其中3bit为数据中心标识,2bit为业务编码,可以设置该值为随机值,放置别人猜测 ID 号。
还有很多拆分方法,这里省略,请大家根据业务需求进行拆分。
Snowflake 算法的不足点
根据上面介绍,已经对雪花算法有了大概的了解,不过雪花算法中部分由时间戳组成,所以其强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。
为了解决这个问题,网上给出了很多方案:
① 关闭时钟同步: 将ID生成交给少量服务器,并关闭时钟同步;
② 抛出异常: 直接抛出异常,交给上层业务处理。
③ 短时间等待: 如果回拨时间较短,在耗时要求内,比如 5ms,那么可以让时钟等待一小段时间,时间到达后再次进行判断,如果已经超过回拨前的时间则正常执行逻辑,否则接着抛出异常。
④ 使用扩展位预防时钟回拨: 如果回拨时间很长,那么无法等待,可以调整算法占用的64位,将 1~2位作为回拨位,一旦时钟回拨将回拨位+1,可得到不一样的ID,2位回拨位允许标记三次时钟回拨,基本够使用。如果超出了,再选择抛出异常。
其中比较推荐的就是使用上面介绍的雪花算法扩展位,如利用 WorkerID 作为扩展位,可以让这 10bit 预留出 2bit,让其作为回滚的标识,当发生时钟回拨时候使其值 +1,由于是 2bit 预留位,所以支持最多三次回拨,一般来说够用,毕竟时钟回拨几率比较小,当然如果还发生了,且超过三次后只能抛出进行处理了。
Snowflake 的 Java 实现示例
这里提供两种方式在 Java 中使用 Snowflake 生成分布式 ID,第一种是使用现成封装好的工具 Hutool,其对 Snowflake 进行了封装,可以直接使用。另一种是自己写代码实现 Snowflake,这种方式可以灵活配置其中的位数分配。
方式一:使用 Hutool 工具封装的 Snowflake 工具
通过 Maven 引入 Hutool 工具包:
1<dependency>
2 <groupId>cn.hutool</groupId>
3 <artifactId>hutool-all</artifactId>
4 <version>5.4.2</version>
5</dependency>
使用 Hutool 中提供的 Snowflake 工具:
1public class SnowflakeHutool {
3 public static void main(String[] args) {
4 // 实例化生成 ID 工具对象
5 Snowflake snowflake = IdUtil.getSnowflake(1, 3);
6 long id = snowflake.nextId();
方式二:自己写代码实现 Snowflake 生成 ID 工具
1import java.util.ArrayList;
2import java.util.HashSet;
3import java.util.List;
5/**
6 * 手动实现 Snowflake 生成 ID 逻辑
8 * @author mydlq
10public class Snowflake {
12 /** 机器id(5位)*/
13 private final long machineId;
14 /** 数据中心id(5位)*/
15 private final long datacenterId;
16 /** 序列号(12位) */
17 private long sequence = 0L;
19 /** 初始时间戳 */
20 private final long INIT_TIMESTAMP = 1288834974657L;
21 /** 机器id位数 */
22 private final long MAX_MACHINE_ID_BITS = 5L;
23 /** 数据中心id位数 */
24 private final long DATACENTER_ID_BITS = 5L;
26 /** 机器id最大值 */
27 private final long MAX_MACHINE_Id = -1L ^ (-1L << MAX_MACHINE_ID_BITS);
28 /** 数据中心id最大值 */
29 private final
long MAX_DATACENTER_ID = -1L ^ (-1L << DATACENTER_ID_BITS);
30 /** 序列号id最大值 */
31 private final long SEQUENCE_BITS = 12L;
32 /** 序列号最大值 */
33 private final long sequenceMask = -1L ^ (-1L << SEQUENCE_BITS);
35 /** workerid需要左移的位数(12位) */
36 private final long WORKER_ID_SHIFT = SEQUENCE_BITS;
37 /** 数据id需要左移位数(12序列号)+(5机器id)共17位 */
38 private final long DATACENTER_ID_SHIFT = SEQUENCE_BITS + MAX_MACHINE_ID_BITS;
39 /** 时间戳需要左移位数(12序列号)+(5机器id)+(5数据中心id)共22位 */
40 private final long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + MAX_MACHINE_ID_BITS + DATACENTER_ID_BITS;
42 /** 上次时间戳,初始值为负数 */
43 private long lastTimestamp = -1L;
45 /**
46 * 构造方法,进行初始化检测
48 * @param machineId 机器ID
49 * @param datacenterId 数据ID
50 */
51 public Snowflake(long machineId, long datacenterId) {
52 // 检查数(机器ID)是否大于5或者小于0
53 if (machineId > MAX_MACHINE_Id || machineId < 0) {
54 throw new IllegalArgumentException(String.format("机器id不能大于 %d 或者小于 0", MAX_MACHINE_Id));
55 }
56 // 检查数(据中心ID)是否大于5或者小于0
57 if (datacenterId > MAX_DATACENTER_ID || datacenterId < 0) {
58 throw new IllegalArgumentException(String.format("数据中心id不能大于 %d 或者小于 0", MAX_DATACENTER_ID));
59 }
60 // 配置参数
61 this.machineId = machineId;
62 this.datacenterId = datacenterId;
63 }
65 /**
66 * 获取下一个生成的分布式 ID
68 * @return 分布式 ID
69 */
70 public synchronized long nextId() {
71 // 获取当前时间戳
72 long currentTimestamp = timeGen();
73 //获取当前时间戳如果小于上次时间戳,则表示时间戳获取出现异常
74 if (currentTimestamp < lastTimestamp) {
75 // 等待 10ms,如果时间回拨时间短,能在 10ms 内恢复,则正常生产 ID,否则抛出异常
76 long offset = lastTimestamp - currentTimestamp;
77 if (offset <= 10) {
78 try {
79 wait(offset << 1);
80 if (currentTimestamp < lastTimestamp) {
81 throw new RuntimeException("系统时间被回调,无法生成ID");
82 }
83 } catch (InterruptedException e) {
84 Thread.currentThread().interrupt();
85 throw new RuntimeException("系统时间被回调,无法生成ID,且等待中断");
86 }
87 }
88 }
89 // 判断当前时间戳是否等于上次生成ID的时间戳(同1ms内),是则进行序列号递增+1,如果递增到设置的最大值(默认4096)则等待
90 if (lastTimestamp == currentTimestamp) {
91 sequence = (sequence + 1) & sequenceMask;
92 if (sequence == 0) {
93 currentTimestamp = tilNextMillis(lastTimestamp);
94 }
95 }
96 // 如果当前时间戳大于上次生成ID的时间戳,说明已经进入下一毫秒,则设置序列化ID为0
97 else {
98 sequence = 0;
99 }
100 // 设置最后时间戳为当前时间戳
101 lastTimestamp = currentTimestamp;
102 // 生成 ID 并返回结果:
103 // (currStamp - INIT_TIMESTAMP) << TIMESTAMP_LEFT_SHIFT 时间戳部分
104 // datacenterId << DATACENTER_ID_SHIFT 数据中心部分
105 // machineId << WORKER_ID_SHIFT 机器标识部分
106 // sequence 序列号部分
107 return ((currentTimestamp - INIT_TIMESTAMP) << TIMESTAMP_LEFT_SHIFT) |
108 (datacenterId << DATACENTER_ID_SHIFT) |
109 (machineId << WORKER_ID_SHIFT) |
110 sequence;
111 }
113 /**
114 * 当某一毫秒时间内产生的ID数超过最大值则进入等待,
115 * 循环判断当前时间戳是否已经变更到下一毫秒,
116 * 是则返回最新的时间戳
117 *
118 * @param lastTimestamp 待比较的时间戳
119 * @return 当前时间戳
120 */
121 private long tilNextMillis(long lastTimestamp) {
122 long timestamp = timeGen();
123 while (timestamp <= lastTimestamp) {
124 timestamp = timeGen();
125 }
126 return timestamp;
127 }
129 /**
130 * 获取系统当前时间
131 *
132 * @return 系统当前时间(毫秒)
133 */
134 private long timeGen() {
135 return System.currentTimeMillis();
136 }
138 /**
139 * 测试 main 方法
140 */
141 public static void main(String[] args) {
142 // 实例化生成 ID 工具对象
143 Snowflake worker = new Snowflake(1, 3);
144 // 创建用于存储 id 的集合
145 List<Long> idList = new ArrayList<>();
146 // 标记开始时间
147 long start = System.currentTimeMillis();
148 // 设置 1000ms 内循环生成 ID
149 while (System.currentTimeMillis() - start <= 1000) {
150 // 生成 ID 加入集合
151 idList.add(worker.nextId());
152 }
153 // 输出1s内生成ID的数量
154 System.out.println("生成 ID 总数量:" + idList.size());
155 }
157}
Snowflake 生成分布式 ID 的优缺点
趋势递增;
可以灵活调整结构;
不需要第数据库等第三方组件依赖;
强依赖时钟,可能发生时钟回拨导致生成的 ID 重复。
方案八:使用 Zookeeper 生成 ID
Zookeeper 中如何生成 ID 值
在 Zookeeper 中主要通过节点数据版本号来生成序列号,可以生成 32 位和 64 位的数据版本号,客户端可以使用这个版本号来作为唯一的序列号。在 Zookeeper 中本身就是支持集群模式,所以能保证高可用性,且生成的 ID 为趋势递增且有序,不过在实际使用中很少用 Zookeeper 来充当 ID 生成器,因为 Zookeeper 中存在强一致性,在高并发场景下其性能可能很难满足需求。
不过由于使用 Zookeeper 节点的版本号来充当 ID 号是比较繁琐,需要创建节点获取生成的 ID,然后去掉节点命令前缀,只截取数字部分,最后还要异步执行删除节点(启动新的线程执行删除节点操作,防止占用生成ID线程执行的实际)。过程比较耗时且繁琐,所以,在操作 Zookeeper 时经经常不会采用该方案,常使用 Curator 客户端提供的基于乐观锁的计数器来自增实现 ID 生成,这个过程和数据库自增生成 ID 类似。
Java 操作 Zookeeper 生成 ID 的实现
在 Java 中可以使用 Curator 包操作 Zookeeper,引入的 Maven 包如下:
1<dependency>
2 <groupId>org.apache.curator</groupId>
3 <artifactId>curator-recipes</artifactId>
4 <version>5.0.0</version>
5</dependency>
6<!-- 创建 Zookeeper 客户端依赖,一定要和 Zookeeper Server 版本保持一致 -->
7<dependency>
8 <groupId>org.apache.zookeeper</groupId>
9 <artifactId>zookeeper</artifactId>
10 <version>3.6.0</version>
11</dependency>
然后使用 curator 工具提供的 increment 方法,使计数器递增,获取递增后的值:
1public class ZookeeperIdExample {
3 /**
4 * 原子递增器对象
6 private DistributedAtomicLong distributedAtomicLong;
8 /**
9 * 初始化操作
10 */
11 @PostConstruct
12 public void init() {
13 // 初始化参数(Zookeeper 地址、Session 超时时间、节点路径、重试策略)
14 String zkServer = "127.0.0.1:2181";
15 int sessionTimeoutMs = 10000;
16 String counterNode = "/distribute_id";
17 RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 10);
18 // 创建 CuratorFramework 对象并启动客户端
19 CuratorFramework cf = CuratorFrameworkFactory.builder()
20 .connectString(zkServer)
21 .sessionTimeoutMs(sessionTimeoutMs)
22 .retryPolicy(retryPolicy)
23 .build();
24 cf.start();
25 // 初始化原子递增器对象
26 distributedAtomicLong = new DistributedAtomicLong(cf, counterNode, retryPolicy);
27 }
29 /**
30 * 获取 ID 号
31 */
32 public Long generateId() {
33 Long generateId = null;
34 try {
35 // 计数器 +1
36 AtomicValue<Long> sequence = distributedAtomicLong.increment();
37 // 判断获取序列号是否是成功的
38 if (sequence.succeeded()) {
39 generateId = sequence.postValue();
40 } else {
41 throw new RuntimeException("生成 ID 异常");
42 }
43 } catch (Exception e) {
44 e.printStackTrace();
45 }
46 // 返回 ID 号
47 return generateId;
48 }
50 public static void main(String[] args) {
51 // 方法类初始化
52 ZookeeperIdExample zookeeperIdExample = new ZookeeperIdExample();
53 zookeeperIdExample.init();
54 // 设置存储生成的 ID 的 List 集合
55 List<Long> idList = new ArrayList<>();
56 // 设置开始时间,该时间作为起始时间
57 long start = System.currentTimeMillis();
58 // 统计 10s 内生成的 ID 数量
59 while (System.currentTimeMillis() - start < 10000){
60 // 生成 ID
61 Long id = zookeeperIdExample.generateId();
62 System.out.println("生成的 ID = " + id);
63 // 加入集合
64 idList.add(id);
65 }
66 System.out.println("大小:" + idList.size());
67 }
使用 Zookeeper 生成 ID 的优缺点
趋势递增;
定期删除之前生成的节点,比较繁琐;
方案九:使用 MongoDB 创建 ObjectID 生成 ID
MongoDB 中如何生成 ID 值
在 MongoDB 中每插入一条数据且没有指定 ID 就会生成一个 _id 键作为唯一标识,该键默认是 ObjectID 串,常常可以类似于像数据库插入数据一样往 MongoDB 中插入数据,获取其默认生成的 ObjectID 值来充当分布式 ID。
MongoDB 的 ObjectId 的组成
在 MongoDB 中默认生成的 ObjectId(十六进制)是由一个 12bit 组成的BSON,ObjectID 是一个 12bit 的 BSON 类型数据,组成类似于雪花算法,如下图就是一个 ObjectID 的组成图:
结构组成如下:
4字节时间戳,以 Unix 纪元以来的秒数为单位(精确到秒)。
5字节随机数。
3字节递增计数器,初始化为随机值,它能确保相同进程同一秒产生的 ObjectId 也是不一样的。同一秒钟最多允许每个进程拥有2563(16 777 216)个不同的 ObjectId。
如下,就是一个十六进制组成的 ObjectID 示例:
15f55e4dddd2ab03204e13167
其中 16 进制转 10 进制如下:
Java 中操作 MongoDB 生成 ID 的实现
在 Java 中可以使用 MongoDB 官方提供的 MongoDB Java 驱动操作 MongoDB,可以如下引入 Maven 包:
1<dependency>
2 <groupId>org.mongodb</groupId>
3 <artifactId>mongo-java-driver</artifactId>
4 <version>3.12.7</version>
5</dependency>
然后使用插入一条数据生成 ID:
1public class MongoExample {
3 public static void main(String[] args) {
4 //连接 MongoDB 服务器,端口号为 27017
5 MongoClient mongoClient = new MongoClient("127.0.0.1", 27017);
6 // 获取数据库(如果就创建不存在就创建)
7 MongoDatabase dbTest = mongoClient.getDatabase("test");
8 // 插入一条文档数据(如果就创建不存在就创建)
9 Document doc = new Document();
10 dbTest.getCollection("myid").insertOne(doc);
11 // 获取 ID 值
12 ObjectId id = (ObjectId) doc.get("_id");
13 // 输出 ID 值
14 System.out.println(id);
15 }
使用 MongoDB 的生成分布式 ID 优缺点
实现简单;
集群模式易于扩展,没有单点问题;
性能一般,只能并发量小的业务需求;
流行的分布式 ID 开源框架
现在网上有很多分布式 ID 开源框架,这里比较常用的有:
滴滴 Tinyid: https://github.com/didi/tinyid
美团 Leaf: https://github.com/Meituan-Dianping/Leaf
百度 Uid-Generator: https://github.com/baidu/uid-generator
这里个开源框架中,大都是按照上面介绍的这几种分布式 ID 生成方案原理开发的,如:
滴滴 Tinyid: 数据库号段模式
百度 Uid-Generator: 数据库 + Snowflake
美团 leaf: 数据库号段模式、Zookeeper + Snowflake
这几款性能都不错,他们的 github 和相关博客中都有详细的介绍,这里我就不过多叙述,感兴趣的博友自行去查阅相关资料。根据本人分析,这几张流行的开源分布式 ID 的实现都做了如下操作:
(1)、减少网络延迟,没有使用 Zookeeper、Redis 等作为分布式锁的核心组件;
(2)、可以灵活配置生成的 ID,可以在其中添加跟业务挂钩的业务号,以满足不同业务需求;
(3)、大部分考虑的是高可用方案,组成统一分布式 ID 分发组件,且组成集群模式,保证可用性;
(4)、将生成的 ID 存入缓存,这样相当于提前往缓存中存入了一批数据,能防止并发突增导致 ID 需求大,也能防止数据库突然不可用。
(5)、都会设置一个监控器和异步更新缓存中分布式 ID 的多个线程,监控器会监控缓存中的使用比例,达到一定比例后会通知更新缓存的线程执行更新分布式 ID 任务,这样会再往缓存中放入一批可以数据。
总结:各种方案的优缺点对比
对部分方案进行了简单测试,由于没有精细的配置组件环境和参数所以这里的数据不一定准确,只供参考:
ID生成速度(单位:s)