


在复杂的分布式系统中,随着业务数据量的爆发式增长,传统的单库单表架构往往难以支撑,分库分表成为了常见的扩展手段。而在分库分表的环境下,如何生成一个全局唯一、高性能且高可用的 ID,成为了架构设计中必须解决的核心问题。
本文将探讨分布式 ID 的由来、核心要求,并详细解析业界主流的开源解决方案:百度的 UidGenerator、滴滴的 TinyID,以及重点剖析美团的 Leaf 系统。
为什么需要分布式 ID?
在单体应用或单库单表中,我们通常使用数据库(如 MySQL)的自增主键(AUTO_INCREMENT)来生成 ID。这种方式简单、可靠,且生成的 ID 有序,对数据库索引友好。
然而,在分库分表的场景下,单纯依赖数据库自增就会遇到问题:
- ID 重复:不同分片上的数据库各自自增,会导致 ID 冲突。
- 扩展困难:无法通过简单的扩容来解决 ID 唯一性问题。
此外,在微服务架构中,订单号、优惠券码、Trace ID 等都需要全局唯一标识。
分布式 ID 的核心要求
一个优秀的分布式 ID 生成系统,通常需要满足以下指标:
- 全局唯一性:这是最基本的要求,不能出现重复 ID。
- 高性能:生成 ID 的延迟要低,QPS 要高,不能成为系统的瓶颈。
- 高可用:ID 生成服务必须极其稳定,一旦宕机可能导致整个业务瘫痪(如无法下单)。
- 趋势递增:最好是趋势递增的(不一定严格连续),这对数据库索引(如 B+ 树)的写入性能非常重要。
- 安全性:如果 ID 严格连续(如 1, 2, 3…),容易被恶意爬取或推测业务量,因此在某些场景下需要 ID 无规则。
常见的基础方案
在介绍大厂方案前,先回顾几种基础方案:
- UUID:
- 优点:本地生成,性能极高,无网络消耗。
- 缺点:太长(128位,36字符),无序,作为 DB 主键会导致 B+ 树频繁分裂,写入性能差。
- 数据库自增(多主模式):
- 利用 MySQL 的
auto_increment_increment和auto_increment_offset设置步长。 - 缺点:扩展性差,强依赖数据库,单点压力大。
- 利用 MySQL 的
- Snowflake(雪花算法):
- Twitter 开源,由 1bit 符号位 + 41bit 时间戳 + 10bit 机器 ID + 12bit 序列号组成。
- 优点:不依赖数据库,高性能,有序。
- 缺点:强依赖机器时钟,时钟回拨会导致 ID 重复或服务不可用。
业界主流开源方案
各大互联网公司基于上述基础方案,针对自身的业务场景和痛点,研发并开源了成熟的分布式 ID 系统。
1. 百度 UidGenerator
UidGenerator 是百度开源的 Java 语言实现的唯一 ID 生成器,基于 Snowflake 算法改进。
核心特点:
- RingBuffer 缓存:与原生 Snowflake 不同,UidGenerator 采用了双 RingBuffer 结构来缓存 ID,采用了生产者-消费者模型。
- 一个 RingBuffer 用于存储生成的 ID。
- 一个 RingBuffer 用于存储状态(Flag)。
- 这种方式使得 ID 的获取操作变成了纯内存读取,性能极高(QPS 可达 600万+)。
- 支持自定义 bit 分配:默认的时间戳位较少(28位),支持约 8.7 年,但可以通过配置调整。
- 解决时钟回拨:通过借用未来时间来解决时钟回拨问题(因为是预生成的)。
适用场景:对性能要求极高,且并发量巨大的场景。
2. 滴滴 TinyID
TinyID 是滴滴开源的分布式 ID 生成系统,它是基于数据库号段模式(Segment)实现的。
核心特点:
- 号段模式:不每次去数据库取 ID,而是每次取一个“号段”(例如 1000 个 ID),加载到内存中慢慢发,用完了再去取。
- 多 DB 支持:支持多 DB 源,提高了可用性。
- TinyID Client(SDK):提供了 Java 客户端。
- 双号段缓存:客户端也会在内存中缓存双号段(当前使用的和下一个备用的)。
- 这意味着即使 TinyID Server 挂了,客户端在一段时间内(号段用完前)还能继续生成 ID,极大地提高了容灾能力。
- HTTP 方式:也支持 HTTP 请求获取 ID,方便非 Java 语言接入。
适用场景:对数据库依赖较高,但希望架构简单、可用性强的场景。TinyID 只支持号段模式,不支持 Snowflake 模式。
3. 美团 Leaf (详细解析)
Leaf 是美团点评开源的分布式 ID 生成系统,它不仅解决了上述方案的许多问题,还提供了极高的稳定性和灵活性。Leaf 这个名字寓意“世界上没有两片完全相同的树叶”。
Leaf 提供了两种生成模式:Leaf-segment(号段模式) 和 Leaf-snowflake(雪花模式)。
3.1 Leaf-segment 模式(号段模式)
Leaf-segment 模式是对数据库自增方案的优化和升级,核心思想是“批量获取”,将数据库的压力降到最低。
1. 数据库设计
Leaf 使用一张表 leaf_alloc 来维护不同业务的 ID 发号进度:
CREATE TABLE `leaf_alloc` (
`biz_tag` varchar(128) NOT NULL DEFAULT '', -- 业务标识,如 order, user
`max_id` bigint(20) NOT NULL DEFAULT '1', -- 当前已分配的最大 ID
`step` int(11) NOT NULL, -- 步长,每次取号段的长度
`description` varchar(256) DEFAULT NULL, -- 描述
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`biz_tag`)
) ENGINE=InnoDB;
2. 获取号段流程
每次 Leaf 服务去数据库取号段时,执行如下事务操作:
UPDATE leaf_alloc SET max_id = max_id + step WHERE biz_tag = #{tag}
SELECT max_id, step FROM leaf_alloc WHERE biz_tag = #{tag}
这样 Leaf 服务就拿到了 [max_id - step, max_id] 这个范围的 ID(号段),可以在内存中慢慢分配,直到用完。
3. 双 Buffer 优化(核心创新)
简单的号段模式有一个问题:当号段用完时,需要同步去 DB 取下一个号段,这会导致瞬间的线程阻塞和 TP999 飙升。Leaf 采用了 双 Buffer 机制来解决这个问题:
- 架构:Leaf 服务内部有两个号段缓冲区(Buffer A 和 Buffer B)。
- 流程:
- 当前服务正在消耗 Buffer A 中的 ID。
- 当 Buffer A 中的 ID 消耗达到一定阈值(如 10% 或 20%)时,后台线程会异步去数据库获取下一个号段,并填充到 Buffer B 中。
- 当 Buffer A 彻底用完时,直接瞬间切换到 Buffer B 继续发号,同时 Buffer A 变成备用,后台线程再次去准备下一个号段填充 Buffer A。
- 效果:除了服务启动时的第一次获取,后续所有的 DB IO 操作都是异步的,获取 ID 的操作变成了纯内存操作,极大地提高了性能和稳定性。
优点:
- 高性能:TP999 极低,ID 生成完全在内存中。
- 高可用:即使数据库宕机,只要内存中的号段还没用完,Leaf 依然可以坚持一段时间(取决于 step 大小)。
- 方便扩容:增加 Leaf 服务节点非常简单,无需分库分表。
缺点:
- ID 不随机:ID 是趋势递增的,竞争对手可能推测出业务量(可以通过混淆 ID 解决)。
- 依赖 DB:虽然有缓存,但最终还是依赖数据库的稳定性。
3.2 Leaf-snowflake 模式(雪花模式)
对于需要 ID 具有时间属性,或者完全不依赖数据库的场景,Leaf 提供了 Snowflake 模式。
1. ID 结构
结构遵循标准的 Snowflake,但对 ZooKeeper 进行了强绑定:
- 1bit:符号位,始终为 0。
- 41bit:时间戳(毫秒级),支持约 69 年。
- 10bit:WorkID(机器 ID),支持 1024 个节点。
- 12bit:序列号,同一毫秒内支持 4096 个 ID。
2. ZooKeeper 管理 WorkID
Leaf 使用 ZooKeeper 持久顺序节点来自动分配 WorkID:
- 启动注册:Leaf 服务启动时,去 ZK 的
/snowflake/${leaf.name}/service/address下创建一个持久顺序节点。 - 获取 ID:ZK 返回生成的顺序 ID,Leaf 将其解析为 WorkID(0-1023)。
- 本地缓存:Leaf 会将获取到的 WorkID 缓存到本地文件(
leaf.properties)。这样即使 ZK 挂了,服务重启时也能读取本地文件恢复 WorkID,属于弱依赖 ZK。
3. 解决时钟回拨(三道防线)
Snowflake 最大的问题是时钟回拨,Leaf 做了周全的防御:
- 启动校验:启动时,检查本机时间是否小于 ZK 上该节点最后上报的时间。如果是,说明时钟回拨,服务拒绝启动并报警。
- 运行时校验:服务运行中,每隔 3 秒向 ZK 上报当前时间。如果发现本机时间小于上次上报时间,说明回拨,暂停服务。
- 备用容错:如果 ZK 挂了,会对比本机时间与本地缓存文件中记录的时间。
优点:
- 高性能:无 DB 依赖,纯内存生成。
- 时间有序:ID 包含时间信息,便于排序和统计。
- 弱依赖 ZK:ZK 短暂故障不影响核心发号服务。
缺点:
- 依赖 ZK:需要维护 ZooKeeper 集群。
- 时钟敏感:虽然有防御机制,但系统时间必须精确。
总结
| 方案 | 核心算法 | 依赖组件 | 优点 | 缺点 |
|---|---|---|---|---|
| 百度 UidGenerator | Snowflake | MySQL + RingBuffer | 性能极高(内存生产),解决回拨 | ID 位数配置较复杂,维护 RingBuffer 稍繁琐 |
| 滴滴 TinyID | Segment | MySQL | 客户端缓存容灾强,架构简单 | 不支持 Snowflake 模式 |
| 美团 Leaf | Segment + Snowflake | MySQL / ZooKeeper | 双模式灵活切换,双 Buffer 优化非常稳定 | 需维护 ZK 或 DB |
在实际选型中:
- 如果希望架构简单,对 ID 连续性要求不高,Leaf-segment 或 TinyID 是很好的选择。
- 如果需要 ID 包含时间信息,且并发量巨大,Leaf-snowflake 或 UidGenerator 更合适。
- 美团 Leaf 由于提供了两种模式的平滑切换和优秀的双 Buffer 设计,是目前业界使用非常广泛的方案。
2238

被折叠的 条评论
为什么被折叠?



