目录
[(一)美团 Leaf (Segment)](#(一)美团 Leaf (Segment))
[(二)美团 Leaf (Snowflake)](#(二)美团 Leaf (Snowflake))
[(三)百度 UidGenerator](#(三)百度 UidGenerator)
[(四)滴滴 Tinyid](#(四)滴滴 Tinyid)
一、业务场景
为确保项目中的每张表记录都能被清晰标识,避免因混淆引发程序逻辑错误,每张表必须至少拥有一个唯一主键。
因此,我们需要一个 ID 生成器。该生成器产生的 ID 必须满足以下三项核心要求:
- 生成不规则性: 连续生成的 ID 易被预测和破解,可能被恶意用于攻击或数据窃取。
- 全局唯一性: 这是最基本也是最重要的要求,必须确保在任何情况下生成的 ID 在整个系统内都是唯一的。
- 单表递增性: 如果生成的 ID 完全无序,会导致DB产生大量随机磁盘写入操作。这不仅会造成吞吐量显著下降,还会导致物理页碎片增加,影响性能和存储效率。
需要注意的是单表递增性其实是分为趋势递增和严格递增,如果严格递增的话我们就得舍弃生成不规则性,所以一般对于严格递增的场景我们都不会使用敏感数据,而是作为辅助字段(比如事务版本号、日志序列号、MQ偏移量等等),它们本身并非业务标识,并不会暴露给外界。
在分布式系统环境中,如何在满足以上核心要求的前提下,设计一种生成速度快、空间占用小且适用于多台机器的 ID 生成方案,是一个至关重要的挑战。
二、方案比对
(一)UUID
java
String id = UUID.randomUUID().toString();
直接通过调用API来快速生成ID,其ID根据生成随机数 + 当前时间戳生成的。
优点:
- 简单易用:无需额外创建生成器组件,直接调用API即可
- 高效:无需网络IO,本地快速生成
- 满足全局唯一性:重复概率极小,几乎为0
- 满足生成不规则性:生成的ID由32位16进制字符随机组成,没有任何规律
缺点:
- 不满足单表递增性:完全随机生成
- 占用空间大:长度为36位,占用索引空间且影响SQL执行效率
可见UUID并没有满足全部的核心要求,是无法作为备用方案考量的。
(二)DB自增
在创建表的时候将主键ID字段设置为自动递增来自动生成ID。
优点:
- 满足单表递增性:绝对有序性,索引效率高
- 简单易用:无需任何操作,依靠DB内部实现
缺点:
- 不满足全局唯一性:一旦分库分表后就会出现相同主键ID的数据
- 不满足生成不规则性:严格递增类型
- 单点故障风险:一旦DB宕机了就无法继续生成ID
- 性能一般:由于是插入DB后才会生成ID,所以生成的效率完全受限于TPS

对于不满足全局唯一性的问题我们可以在分库分表后给每个表设置不同的步长来保障ID全局唯一,但是这样一来水平扩展就变得极其困难。
而且其他不可忽视的缺点也无法解决,因此该方案也并不是一个很好的方案。
(三)Segment号段模式
这个方案可以说是DB自增方案的一个升级版本。
简单来说就是因为DB自增方案性能受限于TPS,所以我们不妨一次性从DB中获取部分ID然后缓存到本地,当用完后再向DB申请即可。
具体来说的话,我们需要在DB中创建一张专门存储号段Segment的表:
sql
CREATE TABLE id_segment (
biz_tag VARCHAR(32) PRIMARY KEY COMMENT '业务标识',
max_id BIGINT NOT NULL COMMENT '当前最大ID',
step INT NOT NULL COMMENT '号段长度'
);
这三个是核心字段,第一个biz_tag作为业务标识,用于区分不同业务之间的号段分配;第二个max_id是当前已经分配的最大id,而每次拿都能拿到max_id + step的号段。
拿取的SQL语句如下:
sql
Begin
UPDATE table SET max_id=max_id+step WHERE biz_tag=xxx
SELECT max_id, step FROM table WHERE biz_tag=xxx
Commit

优点:
- 满足单表递增性:绝对有序性,索引效率高
- 简单易用:无需任何操作,依靠DB内部实现
- 满足全局唯一性:统一ID生成来源,由MySQL行级锁保障原子性递增
- 性能提升:由原先的n次网络请求降至n/step次网络请求
- 有一定容灾性:因为拿到的号段会缓存到本地,所以即使DB宕机了程序依旧可以继续分配剩余ID
缺点:
- 不满足生成不规则性:严格递增类型
- 单点故障风险:即使有一定容灾性但在缓存的号段分配完之后依旧需要面对DB宕机问题
- 性能瓶颈:即使性能相比原方案有很大的提升,但依旧要依赖于网络IO,生成效率还是会逊色一些,如果发生网络波动那更是会造成严重的阻塞问题。
(四)Snowflake雪花算法

符号位用来强保证ID为正整数。
时间戳位是ID的核心,我们设置一个起始时间戳,然后通过计算这个时间戳到当前时间戳的毫秒数来保障ID整体的趋势递增性,由于41bit的限制,所以一个snowflake服务的寿命只有69年。
工作机器ID位是为了避免同一毫秒内不同机器生成的相同ID的问题,用机器ID做区分则保障了唯一性。
序列号位解决的是同一毫秒内同一台机器生成相同ID问题,在同一毫秒内通过分配序列号保障唯一性,如果序列号分配完了则强制后续请求等待下一毫秒。
优点:
- 满足全局唯一性:不同的三个位来强保障ID唯一性,不可能出现重复ID
- 满足生成不规则性:ID由时间戳、机器ID和序列号通过位运算混合而成,难以被预测
- 满足单表递增性:由时间戳位来强保障趋势递增性。
- 性能极高:本地生成,不依赖于网络IO,理论上单机每秒可以生成数万个ID
缺点:
- 时钟回拨风险:如果机器的时钟回拨了的话则会让时间戳回滚导致生成重复ID
- 机器ID需要手动分配
69年后该怎么办?
关于传统的snowflake算法在69年的时间戳耗尽后的解决方案有很多,其中最为推荐的就是引入纪元版本号的方案。
我们可以修改传统ID的结构,减少时间戳、机器ID或序列号位数,将多出来的位数作为纪元版本号方案,放在时间戳位前面,这样的话每当时间戳即将耗尽的时候给纪元版本号+1,由此来保证当前服务的长久寿命。
不过相对的,由于时间戳、机器ID和序列号位数的减少,总寿命以及并发能力就会下降,这里就需要根据实际业务需求做权衡。
为什么要在即将耗尽的时候给版本号+1?
这是为了避免耗尽后到所有机器版本号全部更新版本完成后的这段空窗期,所以提前进行更新。
(五)Redis生成
这个跟DB自增相似,通过redis的INCR命令生成自增ID,由于是在内存操作,所以性能会比DB的好得多,同样号段模式也可以改造成redis生成。
优点:
- 性能提升:内存层面操作
- 满足单表递增性:绝对有序性,索引效率高
- 满足全局唯一性:改造成号段模式后,由于redis执行命令是单线程,所以是原子递增
缺点:
- 不满足生成不规则性:严格递增类型
- 单点故障风险:一旦redis宕机了就无法继续生成ID
- 性能受限:即使改造成号段模式后依旧依赖于网络IO
既然redis的效率比DB要大得多,为什么这个方案使用的不是很广泛?
首先是因为内存比磁盘要贵得多,成本大;其次redis宕机的概率是比DB也要大得多,不是很稳定。
三、企业级开源框架
(一)美团 Leaf (Segment)
针对传统号段模式下发生网络波动的性能瓶颈问题,美团采用了双Buffer模式进行优化:

也就是再创建一个缓冲区,原本的缓冲区存储当前号段,新的缓存区的用于存储下一批号段。
当当前号段消费到10%时如果新缓冲区为空且更新号段进程空闲,则会开启一个异步线程去从DB拉取下一个号段并放入新的缓冲区,这样一来当当前号段分配完成后就可以立马切换缓冲区进行下一号段的分配。
这样就不会出现当号段分配完成后拉取下一号段的空窗期无法处理请求的问题,也给网络波动预留了充足的响应时间。
另外对于单点故障问题,美团则是采用了高可用集群,用一主两从的形式,当主节点宕机时使用DBProxy中间件进行快速切换。
由此一来传统号段模式下两个最为显著的缺陷就被解决了。
如何保障DB主节点的高吞吐量?
传统方案使用的是行级锁来保障,但这样锁冲突的力度就会比较大。
所以美团使用的是乐观锁,在存储号段的表中新增了version字段。
每次拿去号段时都会先查询当前version,然后获取号段时再携带一个version比较的过滤条件,如果对不上就进行重试流程。
但是需要注意的是,上面所说的双Buffer都是在Leaf服务端内存实现的,也就是说客户端每次生成ID都需要网络IO来拉取Leaf生成的ID,这肯定会影响吞吐量,那如何优化?请见下文的滴滴Tinyid。
(二)美团 Leaf (Snowflake)
针对传统snowflake的时钟回拨问题,美团也做了优化:

通过弱依赖于ZooKeeper(只在服务启动时需要用到zk,运行时无需),在服务启动时校验当前机器时间和上传到zk的时间,如果当前时间大于上传时间则代表正常;如果小于的话则需要进一步的判断,若只是阈值内的时钟回拨则稍作等待即可,但如果是超过阈值的严重时钟回拨则直接会报警让人工来回调时钟即可。
然后关于手动分配机器ID也可以交给zk来分配,做到全自动化。
这样一来传统snowflake的两个显著缺陷也得到了解决。
(三)百度 UidGenerator
百度针对于传统snowflake方案做了全面的优化:
首先对于ID的结构,百度在原本的时间戳位中添加了时间基点合并成时间差位,这个时间基点就和上面讲过的纪元版本号是一样的,用于解决传统方案寿命有限的问题。
除此之外,百度UidGenerator还支持配置时间差位、机器ID位和序列号位的位数,便于根据实际业务做权衡。
第二点就是引入了双环形数组的缓冲区:

传统方案在当前毫秒的序列号用尽时会阻塞线程等待下一毫秒,而双环形数组则和美团的双Buffer思想一致,百度UidGenerator启动时会预先生成一批ID放入其中一个环形数组,当分配到阈值的时候则会启动异步线程去生成大于当前数组最后的毫秒值的一批ID放入另一个环形数组,这样一来每次请求就不需要生成ID,只需要从数组拿取即可;而且在序列号耗尽后无需阻塞,直接分配当前数组剩余ID,若当前数组也消费完了就无缝切换另一个数组即可。
由此实现了snowflake的超高吞吐量。
如何保证从数组中拿取ID操作的线程安全?
RingBuffer 使用缓存行填充技术,确保每个槽位独占 CPU 缓存行,避免多线程竞争导致的性能下降,也就是无锁化。
最后一点就是针对时钟回拨问题,百度UidGenerator提供了两种策略,一种就是原生策略:时钟回拨直接抛异常;另一种就是百度自己的策略:阈值内的时钟回拨级别的话则会直接复用最后一次有效的时间戳并开始递增,以损失部分时间精度换取服务持续可用;如果超过阈值的话会将异步更新线程给暂停,然后继续消费当前数组中剩余的ID,如果当前数组剩余ID也分配完后时钟依旧没有恢复才会抛出异常触发外部容错机制。
可以看到百度UidGenerator的目标就是尽量保证服务的持续性。
(四)滴滴 Tinyid
与百度不同,滴滴主要是在DB号段模式方案上下了工夫,可以说是美团Leaf(Segment)衍生出来的方案。
滴滴Tinyid提供了两种模式,一种http模式,类似于DB自增方案的优化,但是性能降低;另一种client模式才是针对号段模式的优化,所以我们这里提到的Tinyid统一为client模式。
滴滴Tinyid相对美团Leaf(Segment)来说要更加灵活,降低了DB单点宕机的风险的同时极大地提高了吞吐量。

所以我们要说的也是上面那三个优化的点。
首先第一点的灵活性,滴滴Tinyid在存储号段的DB表里新增了2个字段:
sql
CREATE TABLE `tiny_id_info` (
`biz_type` VARCHAR(63) NOT NULL COMMENT '业务类型',
`max_id` BIGINT NOT NULL COMMENT '当前最大ID',
`step` INT DEFAULT 0 COMMENT '号段长度',
`delta` INT NOT NULL DEFAULT 1 COMMENT 'ID递增量',
`remainder`INT NOT NULL DEFAULT 0 COMMENT '需满足ID % delta = remainder',
`version` BIGINT NOT NULL COMMENT '版本号'
);
例如:
delta=2, remainder=0 → 0, 2, 4, 6, 8...(偶数序列)
delta=2, remainder=1 → 1, 3, 5, 7, 9...(奇数序列)
delta=3, remainder=1 → 1, 4, 7, 10, 13...(公差为3的序列)
其次就是第二点:降低了DB的单点宕机风险,也就是所谓的高可用设计。
Tinyid有一个专门的server服务端用作对外提供HTTP接口,对内管理号段,当接收到请求后会根据其携带的token进行鉴权,然后再根据biz_type进行路由,也就是说Tinyid支持多DB主节点集群,因此这大大降低了单节点的压力。
最后一点就是吞吐量的极大提升。
上文我们提到了美团Leaf的瓶颈在于每次拉取ID都需要一次网络IO,而滴滴Tinyid则将生成并存储ID的双Buffer嵌入到了客户端本地,也就是说每次拉取ID直接在本地内存拿即可,只在号段即将分配完毕的时候发送一次网络IO到server服务端拉取号段,节省了大量网络IO,所以吞吐量得到了极大的提升。
除此之外还有一点好处就是即使server端宕机了,只要双Buffer里还有ID,服务就还可以持续一段时间,如果是美团Leaf的话则会立刻不可用。
那为什么美团Leaf不选择嵌入到本地客户端?
主要是美团Leaf的开发目的就是为了作为一个单独的服务,可以负责美团的绝大部分业务。而不同的业务的编程语言也不一样,这个时候就只能使用HTTP请求来统一服务所有语言。
而且美团Leaf还支持Snowflake模式,使用HTTP才能将两种模式路由给隔离开来。
总结而言,美团Leaf注重通用性和可控性,而滴滴Tinyid则高度聚焦于Java服务,二者差异是设计初衷的不同所导致的。
四、选型
特性 | 美团 Leaf (Segment) | 美团 Leaf (Snowflake) | 百度 UidGenerator | 滴滴 Tinyid |
---|---|---|---|---|
核心模式 | 数据库号段 + 双Buffer | Snowflake变种 | Snowflake + RingBuffer预取 | 数据库号段 + 本地双Buffer |
有序性 | 严格递增 | 趋势递增 | 趋势递增 | 严格递增 |
性能/吞吐 | 较高 | 很高 | 极高 | 极高 |
优点 | 传统号段模式的全面优化 | 注重服务性的高吞吐,对时钟回拨的处理更清晰 | 吞吐量天花板 | 如果单看Leaf的Segment模式且只考虑Java业务的话,是其升级版本 |
缺点 | HTTP模式下吞吐受网络延迟限制 | 服务化,时钟回拨处理谨慎 | 架构复杂,时钟回拨处理不如Leaf | 依赖客户端SDK集成 |
典型场景 | 非敏感业务ID需求 | 高吞吐、趋势递增的ID需求 | 超高吞吐、趋势递增的ID需求 | 多业务类型的通用ID需求 |
~码文不易,留个赞再走吧~