美团Leaf分布式ID生成系统
一、为什么需要分布式 ID
1.1 背景
在单体应用、单库单表阶段,很多业务表直接使用数据库自增主键就可以满足需求,但当系统逐步演进到以下场景后,传统自增主键方案往往就不够用了:
-
服务拆分,系统进入分布式架构
-
数据量增长,开始分库分表
-
多机房、多实例部署
-
业务希望主键同时承担一定的排序能力
-
对外暴露的 ID 不希望泄露业务规模和数据安全
这时候,业务通常需要一套全局唯一的****分布式 ID 生成方案,用于订单、用户、支付、日志、流水等核心数据的主键或业务标识。
1.2 分布式 ID 的核心诉求
在实际业务中,分布式 ID 往往不只是 “唯一” 这么简单,通常还会同时关注以下几个维度:
1)全局唯一 这是最基础的要求,任何两个不同的数据对象都不能拿到相同的 ID。
2)有序性 有序性通常分为两类:
-
严格单调递增:ID 严格按 1、2、3、4 这样递增,这种形式最理想,但在分布式多节点场景下实现成本较高。
-
趋势递增:整体随时间变大,局部允许乱序,这是分布式场景里更常见的目标,比如 Snowflake 方案。
对于 MySQL InnoDB 这类使用聚簇索引的数据库来说,主键越接近有序,通常越有利于写入性能和索引维护。
3)高性能 要求在高并发场景下仍能稳定生成 ID,具备较高 QPS 和较低延迟。
4)高可用 ID 服务不能成为业务单点。一旦ID生成系统不可用,往往会直接影响下游订单、库存、支付等链路。
5)安全性 如果 ID 具有非常明显的连续规律,外部很容易通过 ID 推测业务量、订单量、用户增长规模,这在很多对外业务中是不可接受的;同时连续 ID 容易被恶意遍历爬取数据,存在数据泄露的安全风险。
6)易运维 分布式 ID 方案往往涉及机器号、时钟、协调组件、数据库等问题,如果运维复杂度过高,长期维护成本会很高。
1.3 为什么没有一种方案能同时完美满足所有诉求
分布式 ID 方案本质上是在多个目标之间做权衡:唯一性、递增性、安全性、性能、可用性、运维复杂度。 例如:
-
严格递增通常意味着中心化协调更多;
-
完全本地生成虽然性能高,但可能带来时钟、机器号分配等问题;
-
安全性强的方案不一定适合做数据库聚簇主键;
-
依赖越少,可用性通常越高,但适用性可能越受限。
所以,分布式 ID 选型的关键不是 “找一个万能方案”,而是根据业务约束选择最合适的方案。
二、常见分布式 ID 方案及其短板
2.1 UUID
UUID是通用唯一识别码(Universally Unique Identifier)的缩写。它通过一定的算法机器生成,包括网卡MAC地址、时间戳、随机或伪随机数、时序等元素,UUID的标准型式包含32个16进制数字,以连字号分为五段,形式为8-4-4-4-12的36个字符。
UUID uuid = UUID.randomUUID();
uuid:9fea8ab3-0789-4719-aa70-8849cbc59916
-
优点:本地生成、无网络依赖、实现简单、可用性极高、无单点问题
-
缺点:
-
无序:因为生成无规律,大量的随机添加势必导致MySQL底层大量的B+ tree的节点分裂,耗费大量计算资源,严重影响数据库性能
InnoDB 主键使用的是聚簇索引,这意味着数据在物理存储上是按照主键的顺序依次排列的。如果主键是自增有序的,每次插入新数据,数据库只需在当前数据页的末尾“追加”即可,效率极高。但如果主键是无序的随机 UUID,数据库在插入新数据时,往往需要将其强行塞入到之前已经写满的数据页中间。为了腾出空间,数据库不得不把满页的数据拆分成两个页,这就是页分裂(Page Split)。大量的页分裂会导致频繁的磁盘数据移动、产生大量内存碎片,并极大地降低插入(Insert)的并发性能。
-
字符串:存储占用空间大,查询、排序效率低
B+ 树节点容量降低: 数据库(如 MySQL InnoDB)的主键索引使用的是 B+ 树,其每个节点(页)的大小是固定的(默认 16KB)。主键的体积越大,一个节点能容纳的主键数量就越少。这会导致 B+ 树的层级变深,查询时需要的磁盘 I/O 次数增加,直接拖慢查询速度。
二级索引体积膨胀: 在 InnoDB 中,辅助索引(非主键索引)的叶子节点存储的是主键的值。这意味着,如果你有一个 36 字节的 UUID 作为主键,那么你表里的每一个二级索引都会多出极大的存储开销。表越大、索引越多,浪费的磁盘和内存空间就越惊人。相比之下,BIGINT 只需要 8 个字节。
比较效率极低:在数据库执行查询、排序或多表 Join 时,底层需要频繁比对主键的值。CPU 对整型(如 BIGINT)的比较是原生的指令,速度极快;而对字符串的比较,需要逐个字符进行查对。
-
适用场景:对入库性能不敏感、不做主键,仅做业务标识;
不适用:高并发 MySQL 主表主键、索引敏感的核心表。
2.2 数据库自增ID
利用 MySQL auto_increment 特性,单表自增生成ID,我们也可以专门使用一张数据表生成自增唯一且有序的分布式ID:
CREATE TABLE id_allocation (
id INT AUTO_INCREMENT PRIMARY KEY
);
-
优点:实现简单、ID 严格递增天然唯一
-
缺点:
1)单点瓶颈:单实例 DB 成为性能与故障瓶颈;
2)扩展性差:分库分表、多机房无法复用;
3)ID 连续,容易推算业务体量,安全性差。
适用场景:小型单体、单库单表、对内非敏感业务。
2.3 多库步长自增
针对上述问题,可以使用采用「起始偏移 + 固定步长」的多库自增方案,来提高多库多表生成分布式ID以保证高可用,例如可以使用多主模式,每个库分别存放一张id分配表(id_allocation )或者分表,表1的id从1开始,表2的id从2开始,表3的id从3开始,各自的步长都为3
-- ID分配表1,从1开始自增,步长为3
CREATE TABLE db1.id_allocation (
id INT AUTO_INCREMENT PRIMARY KEY
) AUTO_INCREMENT=1;
-- ID分配表2,从2开始自增,步长为3
CREATE TABLE db2.id_allocation (
id INT AUTO_INCREMENT PRIMARY KEY
) AUTO_INCREMENT=2;
-- ID分配表3,从3开始自增,步长为3
CREATE TABLE db3.id_allocation (
id INT AUTO_INCREMENT PRIMARY KEY
) AUTO_INCREMENT=3;
-
优点:解决单库单点,性能问题,ID 整体有序;
-
缺点:
-
不利于后续的扩展,比较复杂,维护成本较高
-
单个数据库压力还是很大,无法满足高并发需求
-
2.4 数据库号段模式
号段式分布式ID生成是当下比较主流的实现方案之一,它的整体思路是在专门建立一张数据表划分当前业务的ID分配段,每次加载一批号段到内存中,然后所有的服务都从这个号码段中获取ID,直至这个号码段用完,然后再到数据库中再划动一批到内存中使用
CREATE TABLE id_allocation (
id int(10) NOT NULL,
max_id bigint(20) NOT NULL COMMENT '当前已用最大id',
step int(20) NOT NULL COMMENT '号段分配步长',
biz_type int(20) NOT NULL COMMENT '业务类型',
version int(20) NOT NULL COMMENT '版本号',
PRIMARY KEY (`id`)
)
update id_generator set max_id = #{max_id+step}, version = version + 1 where version = # {version} and biz_type = XXX
-
优点:减少 DB 访问频次,内存发号吞吐高;DB 临时故障,存量号段可继续发号。
-
缺点:
-
ID 连续,泄露业务量;
-
号段耗尽同步拉取 DB,请求阻塞,TP99 延迟毛刺;
-
底层依赖 MySQL,数据库宕机无新号段可用。
-
2.5 Redis 自增
Redis分布式ID实现主要是通过提供像INCR 和 INCRBY 这样的自增原子命令,由于Redis单线程的特点,可以保证ID的唯一性和有序性
-
优点:性能高、实现简单、ID有序
-
缺点:
-
依赖Redis,集群/主从切换存在一致性风险
-
高并发下Redis仍有网络开销与压力,若Redis有阻塞命令,影响可用性
-
2.6 原生雪花算法(Snowflake)
雪花算法是twitter开发是一中id生成算法,将64位二进制拆分(时间戳+机器ID+序列号),本地计算生成ID。
-
优点:
-
在毫秒级情况下可以生成大量id,纯内存生成性能出色,毫秒数在高位,自增序列在低位趋势递增,类型也是long类型,且包含日期和workerid等具备基因特性的因子,可以满足大部分业务场景的需求
-
不依赖数据库,分布式扩展能力强
-
可以根据自身业务特性分配bit位,非常灵活(百度的uid-generator)
-
-
缺点:
-
WorkerID: 人工配置,集群扩缩容易重复,ID 冲突风险
-
时钟漂移:强依赖服务器时钟,时间漂移风险大
计算机的时间并非凭空而来,而是靠主板上的石英晶体振荡器和纽扣电池来模拟的。通常这块晶体的振动频率为 32,768Hz(即每振动 32,768 次代表 1 秒过去)。 但物理硬件并不完美,受极端环境(如低温)影响,晶体振动会出现误差。在正常情况下,服务器每天也会产生±1s左右的计时误差,这种现象被称为“时钟漂移”。
- 时钟回拨:雪花算法有41位是时间戳组成,NTP 时间同步可能引发时钟回拨,同 WorkerID + 同时间戳生成重复 ID
既然单台机器的时间会飘,集群环境怎么保证时间一致?1985 年,David L. Mills 设计了NTP(网络时间协议****),它可以将局域网内的机器时钟误差强行拉平到 1ms 以内。 但 NTP 的介入带来了一个极其恐怖的副作用:如果某台机器的时钟跑得太快了,NTP 会把它的时间往回拨!
-
三、Leaf整体设计思路
Leaf 是美团开源分布式 ID 框架:
Leaf 提供两套独立实现模式,两套模式可并行部署,按需选用:
-
Leaf-segment:优化版数据库号段,主打严格自增、无时钟依赖;
-
Leaf-snowflake:增强版雪花,主打有序安全、趋势递增。
3.1 Leaf-segment:数据库号段模式的工程化增强
3.1.1 核心原理
基于预分配号段 + 本地内存缓存,批量从数据库获取ID区间,业务请求直接从内存发号,号段消耗至阈值,后台异步预加载下一个号段,大幅降低DB压力。
3.1.2 数据库表设计
CREATE TABLE leaf_alloc (
biz_tag VARCHAR(128) NOT NULL COMMENT '业务标识',
max_id BIGINT(20) NOT NULL DEFAULT 1 COMMENT '当前最大已分配ID',
step INT(11) NOT NULL COMMENT '号段步长',
desc VARCHAR(256) DEFAULT NULL COMMENT '业务描述',
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (biz_tag)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
biz_tag:业务隔离;max_id:已分配上限;step:单次申领数量。
3.1.3 三大核心优化点
- 双 Buffer,消除号段切换阻塞
设计主、备两个号段容器;当当前号段消耗90%水位线,异步线程提前拉取下一个号段填充备用Buffer;当前号段用尽后无缝切换,业务请求全程无阻塞。
- 解决数据库单点问题 → DB高可用架构
底层MySQL采用一主多从 + 故障自动切换,配合中间件实现主从转移,规避单库宕机风险;同时合理调大步长,DB宕机后,内存号段仍可支撑10~20分钟正常发号。
- 动态 step 自适应
号段更新时实时持久化max_id,服务重启后从数据库加载最新位置,服务运行过程中,会根据单个buffer消耗时长动态调整步长(比如:消耗时长小于15分钟,步长会加倍,消耗时长大于30分钟,步长会缩小原来1/2),在 DB 压力、ID 浪费中间动态平衡:
-
短时间耗完号段→step 自动扩容,减少 DB 查询;
-
长时间闲置号段→step 自动收缩,减少服务重启 ID 空洞;
3.1.4 优缺点 & 适用场景
-
优点:严格单调递增、无时钟依赖、内存发号高性能、容灾强;
-
缺点:ID 连续不安全、重启存在少量 ID 空洞、依赖 MySQL;
-
适用:内部日志、流水、报表等非敏感业务;
-
禁用:订单、用户 ID 等对外业务。
3.2 Leaf-snowflake 增强雪花模式
3.2.1 原生Snowflake结构复用
64位ID结构:1位符号位 + 41位毫秒时间戳 + 10位WorkerID + 12位序列号
-
毫秒级时间戳:可用约69年
-
10位WorkerID:支持最多1024个节点
-
12位序列号:单毫秒内支持4096个ID
3.2.2 针对「原生Snowflake」核心缺点的优化
问题1:WorkerID 手动分配,运维复杂、易冲突
Leaf 解决方案:基于ZooKeeper自动分配WorkerID
- 服务启动后连接ZK,创建持久顺序节点,利用ZK自增顺序号作为当前实例的WorkerID,全集群自动分配,无需人工配置,扩缩容零运维成本,彻底避免ID冲突
持久顺序节点:/snowflake/{leaf.name}/forever/{ip}:{port}-000000001
例如:/snowflake/leaf/forever/192.168.1.100:8080-000000001
- 本地文件缓存WorkerID,ZK宕机重启可复用已有ID,降低强依赖;
本地文件:{java.io.tmpdir}/{leaf.name}/leafconf/{port}/workerID.properties
例如:/tmp/leaf/leafconf/8080/workerID.properties
文件内容:workerID=123
问题2:时钟回拨,导致ID重复(最致命问题)
Leaf 多层时钟回拨防护体系
-
启动校验:启动时对比本机时间与ZK记录的历史最大时间,若发生回拨直接启动失败并告警;
-
运行时实时监控:定时检测系统时间,小幅回拨(≤5ms)主动阻塞等待时间追平,不生成重复ID;
-
大幅时间回拨:直接拒绝服务并触发报警,防止脏数据;
-
运维兜底建议:生产环境关闭节点NTP自动时间同步,从源头减少时钟回拨概率;
3.2.3 优缺点 & 适用场景
-
优点:ID 无序防爬虫、趋势递增、纯内存高性能、自动分配机器 ID 运维简单;
-
缺点:新节点扩容依赖 ZK、无法彻底规避硬件时钟漂移;
-
适用:订单、支付、用户 ID 等对外核心敏感业务。
四、全方案横向对比
| 实现方案 | 递增性 | 安全性(防泄露) | 性能 | 可用性 | 核心缺陷 |
|---|---|---|---|---|---|
| UUID | 无序 | 中 | 高 | 极高 | 索引性能差 |
| 数据库自增 | 严格递增 | 差(连续) | 低 | 低 | DB单点、分库重复 |
| 多库步长自增 | 整体递增 | 低 | 中 | 中 | 扩容复杂、维护成本高 |
| 简易号段 | 严格递增 | 差(连续) | 中高 | 中 | 阻塞、DB单点 |
| Redis自增 | 严格递增 | 差(连续) | 高 | 中 | 依赖Redis、ID连续 |
| 原生Snowflake | 趋势递增 | 高 | 极高 | 中 | 时钟回拨、WorkerID难分配 |
| Leaf-segment | 严格递增 | 差(连续) | 极高 | 极高 | ID连续、少量空洞 |
| Leaf-snowflake | 趋势递增 | 高 | 极高 | 高 | 依赖ZK、时钟风险 |
选型原则
选 segment:内部业务、需要严格自增、不在意 ID 连续泄露;
选 snowflake:对外核心交易、防爬保密、只需趋势递增;
不推荐:绝对无空洞连续编号、无力做时钟运维的极简小项目。

浙公网安备 33010602011771号