Leaf 分布式 ID 生成实战——美团 Leaf 从原理到落地分库分表后自增主键废了、雪花算法机器号怎么分配、两步一个 ID 的数据库方式扛不住高并发——美团 Leaf 专治这些。号段模式用双 Buffer 预加载把 DB 查 ID 从两步优化到近零步雪花模式用 ZooKeeper 自动分配 Worker ID 解决机器号冲突。本文从两种模式的原理讲起到 DB 建表、部署配置。为什么需要分布式 ID单机 MySQL 时代AUTO_INCREMENT一把梭搞定。微服务分库分表后一张表拆成 16 张分片——自增主键没法用了因为 16 张表各自从 1 开始递增全局一定出现重复 ID。分布式 ID 的核心要求就两条全局唯一高性能每秒能生成几万到几十万个常用的方案方案优点缺点UUID简单本地生成太长、无序BTree 索引不友好自增主键 步长DB 号段有序、全局唯一每次取号要查 DB不够快Redis 自增快Redis 持久化非强项挂了丢数据雪花算法本地生成极快强依赖机器时钟时钟回拨可能产生重复 ID美团 Leaf 把 DB 号段和雪花算法两个方案都收了进来各取所长号段模式SegmentDB 批量取号服务端缓存 双 Buffer 预加载DB 一跳抵一万次雪花模式Snowflake纯本地生成用 ZooKeeper 自动分配 Worker ID解决时钟回拨号段模式——怎么做到又快又唯一思路号段模式的核心就是一次从 DB 取一批 ID在内存里慢慢用用完了再取下一批。MySQLLeaf 服务业务服务MySQLLeaf 服务业务服务这批号段[1001, 2000]服务端缓存 1000 个 IDalt[还有剩余][用完了]我要一个 IDkeyuser内存里还有吗直接返回 IDUPDATE leaf_alloc SET max_id max_id step WHERE biz_tag user新的 max_id2000, step1000返回 ID 1001这样一来取 1000 个 ID 才查一次 DB。step越大DB 压力越小但万一 Leaf 服务挂了没发出去的那批 ID 就丢了不过问题不大ID 不要求连续跳号无所谓。双 Buffer——用的时候后台已经在准备下一批了上面的方案还有一个问题每次号段用完时业务请求要等着 Leaf 去查 DB——这 10ms 左右的延迟对高并发场景来说还是有影响。Leaf 的解法是双 Buffer每个 key 维护两个号段当前号段快用完比如用了 90%时后台异步线程去 DB 把下一个号段拉回来备着。当前号段用完后直接切到备用的毫无停顿。内存中每个 biz_tag 的双 Buffervalue → 1501没到 max2000Segment A当前使用value1500, max2000Segment B备用已就绪value2000, max3000请求来了要 ID返回 1501A 用了 90% 时后台线程去 DB 预加载Segment B 提前就绪DB 表结构CREATEDATABASEleaf;USEleaf;CREATETABLEleaf_alloc(biz_tagVARCHAR(128)NOTNULLDEFAULTCOMMENT业务标识比如 user、order,max_idBIGINTNOTNULLDEFAULT1COMMENT当前已分配的最大 ID,stepINTNOTNULLDEFAULT1000COMMENT每次分配的号段长度,descriptionVARCHAR(256)DEFAULTNULLCOMMENT业务描述,update_timeTIMESTAMPNOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMP,PRIMARYKEY(biz_tag));插入一条初始数据INSERTINTOleaf_alloc(biz_tag,max_id,step,description)VALUES(user,1,1000,用户表 ID);号段模式的原理就这一条 SQL-- 取号的时候max_id max_id step然后返回新的 max_idUPDATEleaf_allocSETmax_idmax_idstepWHEREbiz_taguser;-- 再查出来当前的 max_id 是多少SELECTbiz_tag,max_id,stepFROMleaf_allocWHEREbiz_taguser;Leaf 服务拿到max_id1001那就知道这批号段是[max_id - step 1, max_id][2, 1001]一共 1000 个 ID 可用。雪花模式——纯内存生成用 ZooKeeper 管 Worker ID号段模式需要一个独立部署的 Leaf 服务雪花模式不需要——它是在业务服务本地直接生成 ID性能天花板更高。雪花算法原理一个 64 位 long 型 ID按位分成四段┌─┬──────────────────────────────────────┬────────────┬──────────┐ │1│ 41 位时间戳 │ 10 位机器号 │ 12 位序号 │ │ │ 毫秒级 │Worker ID│ │ │0│ │ │ │ └─┴──────────────────────────────────────┴────────────┴──────────┘ 示例71022798411251713 拆解 时间戳部分 1692625699相对时间 机器号 10 序号 11 位固定为 0保证生成的 ID 是正数41 位时间戳毫秒级可以支撑 69 年从起始时间算起10 位 Worker ID机器号最多支持 1024 个节点12 位序号同一毫秒内最多生成 4096 个 ID先解释一下Worker ID 到底是干嘛的。你部署了 3 台服务器每台都在跑雪花算法生成 ID。假设第一台和第二台在同一毫秒同时生成了一个 ID——如果两台服务器没有区分标识时间戳一样、序号一样生成的 ID 就一模一样。服务器 A时间戳1692625699000 序号1 → 生成的 ID 71022798411251712 服务器 B时间戳1692625699000 序号1 → 生成的 ID 71022798411251712 ↑ 一模一样所以雪花算法留了 10 位给每台机器一个唯一编号——Worker ID。同一毫秒不同机器生成的 IDWorker ID 不同整个 ID 就不同服务器 AWorker ID1 时间戳 0000000001 序号 → ID xxx...1...001 服务器 BWorker ID2 时间戳 0000000010 序号 → ID xxx...2...001 ↑ 不同机器全局唯一单机部署Worker ID 写死为 0 就行永远不冲突。多机部署每台机器的 Worker ID 必须全局唯一。问题就变成了——这 1024 个编号谁来分配怎么保证不重复Leaf 的做法是用 ZooKeeper 自动分配。每台机器启动时去 ZK 注册ZK 给它一个唯一的编号。省掉了手动改配置、担心配重的麻烦。所以理论峰值 QPS 4096 × 1000 409 万/秒。一个机器每毫秒 4096 个 ID日常够用。雪花算法最大的坑——时钟回拨服务器 NTP 校时把系统时间往回拨了 2 秒。雪花算法依赖时间戳递增来保证 ID 不重复时间往回走了→可能生成跟上一秒一模一样的 ID→数据库插入冲突。Leaf 的应对回拨不超过 5mssleep 等待等时间追上之前的时间戳回拨超过 5ms直接拒绝生成返回异常不停留在可能出错的状态用 ZooKeeper 维护 Worker ID每次启动时去 ZK 注册ZK 会分配一个全局唯一的 Worker ID。同时 Leaf 会往 ZK 周期性上报当前时间戳如果重启后发现本地时间比 ZK 记录的值还小→抛出异常ZooKeeper 怎么分配 Worker IDZooKeeperLeaf 实例IP192.168.1.5ZooKeeperLeaf 实例IP192.168.1.5节点编号 000000003Worker ID 3每 3 秒往节点上报当前时间戳重启时先检查 ZK 记录的时间戳如果本地时间 ZK 记录的时间 → 拒绝启动创建持久顺序节点/snowflake/forever/192.168.1.5:2222-000000003返回 Worker ID 3更新节点数据{ip, port, timestamp}项目结构参考 Leaf 官方把项目下载下来下面目录结构中的 core 从 leaf-core 模块中拷贝其他 controller、service、constant 从 leaf-server 模块拷贝。完整拷贝过来之后有些包路径改一改就行改成自己的。另外我们还需要建个数据库mysql 数据库的CREATETABLEleaf_alloc(biz_tagvarchar(128)NOTNULLDEFAULT,max_idbigint(20)NOTNULLDEFAULT1,stepint(11)NOTNULL,descriptionvarchar(256)DEFAULTNULL,update_timetimestampNOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMP,PRIMARYKEY(biz_tag))ENGINEInnoDB;insertintoleaf_alloc(biz_tag,max_id,step,description)values(leaf-segment-test,1,2000,Test leaf Segment Mode Get Id)之后在 pom.xml 里面因为这个项目比较老了所以我们包的依赖管理就单独在这个 biz 模块就可以了不放在父工程包依赖管理了propertiescommon-io.version2.4/common-io.versionperf4j.version0.9.16/perf4j.versiondruid.version1.0.18/druid.versionmybatis.version3.3.0/mybatis.versioncurator-recipes.version2.6.0/curator-recipes.versionzookeeper.version3.6.0/zookeeper.version/properties!-- 如果要服务注册feign 调用等自己加这里不多介绍 --dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependencydependencygroupIdcommons-io/groupIdartifactIdcommons-io/artifactIdversion${common-io.version}/version/dependencydependencygroupIdorg.perf4j/groupIdartifactIdperf4j/artifactIdversion${perf4j.version}/version/dependencydependencygroupIdcom.mysql/groupIdartifactIdmysql-connector-j/artifactId/dependencydependencygroupIdcom.alibaba/groupIdartifactIddruid/artifactIdversion${druid.version}/version/dependencydependencygroupIdorg.mybatis/groupIdartifactIdmybatis/artifactIdversion${mybatis.version}/version/dependency!-- zk --dependencygroupIdorg.apache.curator/groupIdartifactIdcurator-recipes/artifactIdversion${curator-recipes.version}/version!-- 为防止日志冲突添加以下排除项 --exclusionsexclusionartifactIdlog4j/artifactIdgroupIdlog4j/groupId/exclusionexclusionartifactIdorg.slf4j/artifactIdgroupIdslf4j-reload4j/groupId/exclusionexclusiongroupIdorg.slf4j/groupIdartifactIdslf4j-api/artifactId/exclusionexclusiongroupIdorg.apache.zookeeper/groupIdartifactIdzookeeper/artifactId/exclusion/exclusions/dependencydependencygroupIdorg.apache.zookeeper/groupIdartifactIdzookeeper/artifactIdversion${zookeeper.version}/version!-- 为防止日志冲突添加以下排除项 --exclusionsexclusiongroupIdorg.slf4j/groupIdartifactIdslf4j-api/artifactId/exclusionexclusiongroupIdorg.slf4j/groupIdartifactIdslf4j-log4j12/artifactId/exclusionexclusiongroupIdlog4j/groupIdartifactIdlog4j/artifactId/exclusion/exclusions/dependency还要在 /service 包下修改一下 SegmentService因为我用的是 mysql8.x如果你不是可以不用改如果你想为某个业务添加全局ID比如用户ID、帖子ID要在数据库里面执行一个插入SQLINSERTINTOleaf.leaf_alloc(biz_tag,max_id,step,description,update_time)VALUES(user-id,10100,2000,用户 ID,now());# 分别是业务唯一表示biz-tag# ID 起始值# 步长# 描述信息另外第二种雪花算法是基于 Zookeeper所以如果要用的话就得配置上leaf.nameidle-store leaf.segment.enabletrue leaf.jdbc.urljdbc:mysql://127.0.0.1:3306/leaf?useUnicodetruecharacterEncodingutf-8autoReconnecttrueuseSSLfalseserverTimezoneAsia/Shanghai leaf.jdbc.usernameroot leaf.jdbc.passwordHliu1026. leaf.snowflake.enabletrue leaf.snowflake.zk.address127.0.0.1:2181 leaf.snowflake.port2222默认两个都是关闭的用哪个就要配置哪个。
Leaf 分布式 ID 生成实战——美团 Leaf 从原理到落地
发布时间:2026/7/11 4:28:40
Leaf 分布式 ID 生成实战——美团 Leaf 从原理到落地分库分表后自增主键废了、雪花算法机器号怎么分配、两步一个 ID 的数据库方式扛不住高并发——美团 Leaf 专治这些。号段模式用双 Buffer 预加载把 DB 查 ID 从两步优化到近零步雪花模式用 ZooKeeper 自动分配 Worker ID 解决机器号冲突。本文从两种模式的原理讲起到 DB 建表、部署配置。为什么需要分布式 ID单机 MySQL 时代AUTO_INCREMENT一把梭搞定。微服务分库分表后一张表拆成 16 张分片——自增主键没法用了因为 16 张表各自从 1 开始递增全局一定出现重复 ID。分布式 ID 的核心要求就两条全局唯一高性能每秒能生成几万到几十万个常用的方案方案优点缺点UUID简单本地生成太长、无序BTree 索引不友好自增主键 步长DB 号段有序、全局唯一每次取号要查 DB不够快Redis 自增快Redis 持久化非强项挂了丢数据雪花算法本地生成极快强依赖机器时钟时钟回拨可能产生重复 ID美团 Leaf 把 DB 号段和雪花算法两个方案都收了进来各取所长号段模式SegmentDB 批量取号服务端缓存 双 Buffer 预加载DB 一跳抵一万次雪花模式Snowflake纯本地生成用 ZooKeeper 自动分配 Worker ID解决时钟回拨号段模式——怎么做到又快又唯一思路号段模式的核心就是一次从 DB 取一批 ID在内存里慢慢用用完了再取下一批。MySQLLeaf 服务业务服务MySQLLeaf 服务业务服务这批号段[1001, 2000]服务端缓存 1000 个 IDalt[还有剩余][用完了]我要一个 IDkeyuser内存里还有吗直接返回 IDUPDATE leaf_alloc SET max_id max_id step WHERE biz_tag user新的 max_id2000, step1000返回 ID 1001这样一来取 1000 个 ID 才查一次 DB。step越大DB 压力越小但万一 Leaf 服务挂了没发出去的那批 ID 就丢了不过问题不大ID 不要求连续跳号无所谓。双 Buffer——用的时候后台已经在准备下一批了上面的方案还有一个问题每次号段用完时业务请求要等着 Leaf 去查 DB——这 10ms 左右的延迟对高并发场景来说还是有影响。Leaf 的解法是双 Buffer每个 key 维护两个号段当前号段快用完比如用了 90%时后台异步线程去 DB 把下一个号段拉回来备着。当前号段用完后直接切到备用的毫无停顿。内存中每个 biz_tag 的双 Buffervalue → 1501没到 max2000Segment A当前使用value1500, max2000Segment B备用已就绪value2000, max3000请求来了要 ID返回 1501A 用了 90% 时后台线程去 DB 预加载Segment B 提前就绪DB 表结构CREATEDATABASEleaf;USEleaf;CREATETABLEleaf_alloc(biz_tagVARCHAR(128)NOTNULLDEFAULTCOMMENT业务标识比如 user、order,max_idBIGINTNOTNULLDEFAULT1COMMENT当前已分配的最大 ID,stepINTNOTNULLDEFAULT1000COMMENT每次分配的号段长度,descriptionVARCHAR(256)DEFAULTNULLCOMMENT业务描述,update_timeTIMESTAMPNOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMP,PRIMARYKEY(biz_tag));插入一条初始数据INSERTINTOleaf_alloc(biz_tag,max_id,step,description)VALUES(user,1,1000,用户表 ID);号段模式的原理就这一条 SQL-- 取号的时候max_id max_id step然后返回新的 max_idUPDATEleaf_allocSETmax_idmax_idstepWHEREbiz_taguser;-- 再查出来当前的 max_id 是多少SELECTbiz_tag,max_id,stepFROMleaf_allocWHEREbiz_taguser;Leaf 服务拿到max_id1001那就知道这批号段是[max_id - step 1, max_id][2, 1001]一共 1000 个 ID 可用。雪花模式——纯内存生成用 ZooKeeper 管 Worker ID号段模式需要一个独立部署的 Leaf 服务雪花模式不需要——它是在业务服务本地直接生成 ID性能天花板更高。雪花算法原理一个 64 位 long 型 ID按位分成四段┌─┬──────────────────────────────────────┬────────────┬──────────┐ │1│ 41 位时间戳 │ 10 位机器号 │ 12 位序号 │ │ │ 毫秒级 │Worker ID│ │ │0│ │ │ │ └─┴──────────────────────────────────────┴────────────┴──────────┘ 示例71022798411251713 拆解 时间戳部分 1692625699相对时间 机器号 10 序号 11 位固定为 0保证生成的 ID 是正数41 位时间戳毫秒级可以支撑 69 年从起始时间算起10 位 Worker ID机器号最多支持 1024 个节点12 位序号同一毫秒内最多生成 4096 个 ID先解释一下Worker ID 到底是干嘛的。你部署了 3 台服务器每台都在跑雪花算法生成 ID。假设第一台和第二台在同一毫秒同时生成了一个 ID——如果两台服务器没有区分标识时间戳一样、序号一样生成的 ID 就一模一样。服务器 A时间戳1692625699000 序号1 → 生成的 ID 71022798411251712 服务器 B时间戳1692625699000 序号1 → 生成的 ID 71022798411251712 ↑ 一模一样所以雪花算法留了 10 位给每台机器一个唯一编号——Worker ID。同一毫秒不同机器生成的 IDWorker ID 不同整个 ID 就不同服务器 AWorker ID1 时间戳 0000000001 序号 → ID xxx...1...001 服务器 BWorker ID2 时间戳 0000000010 序号 → ID xxx...2...001 ↑ 不同机器全局唯一单机部署Worker ID 写死为 0 就行永远不冲突。多机部署每台机器的 Worker ID 必须全局唯一。问题就变成了——这 1024 个编号谁来分配怎么保证不重复Leaf 的做法是用 ZooKeeper 自动分配。每台机器启动时去 ZK 注册ZK 给它一个唯一的编号。省掉了手动改配置、担心配重的麻烦。所以理论峰值 QPS 4096 × 1000 409 万/秒。一个机器每毫秒 4096 个 ID日常够用。雪花算法最大的坑——时钟回拨服务器 NTP 校时把系统时间往回拨了 2 秒。雪花算法依赖时间戳递增来保证 ID 不重复时间往回走了→可能生成跟上一秒一模一样的 ID→数据库插入冲突。Leaf 的应对回拨不超过 5mssleep 等待等时间追上之前的时间戳回拨超过 5ms直接拒绝生成返回异常不停留在可能出错的状态用 ZooKeeper 维护 Worker ID每次启动时去 ZK 注册ZK 会分配一个全局唯一的 Worker ID。同时 Leaf 会往 ZK 周期性上报当前时间戳如果重启后发现本地时间比 ZK 记录的值还小→抛出异常ZooKeeper 怎么分配 Worker IDZooKeeperLeaf 实例IP192.168.1.5ZooKeeperLeaf 实例IP192.168.1.5节点编号 000000003Worker ID 3每 3 秒往节点上报当前时间戳重启时先检查 ZK 记录的时间戳如果本地时间 ZK 记录的时间 → 拒绝启动创建持久顺序节点/snowflake/forever/192.168.1.5:2222-000000003返回 Worker ID 3更新节点数据{ip, port, timestamp}项目结构参考 Leaf 官方把项目下载下来下面目录结构中的 core 从 leaf-core 模块中拷贝其他 controller、service、constant 从 leaf-server 模块拷贝。完整拷贝过来之后有些包路径改一改就行改成自己的。另外我们还需要建个数据库mysql 数据库的CREATETABLEleaf_alloc(biz_tagvarchar(128)NOTNULLDEFAULT,max_idbigint(20)NOTNULLDEFAULT1,stepint(11)NOTNULL,descriptionvarchar(256)DEFAULTNULL,update_timetimestampNOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMP,PRIMARYKEY(biz_tag))ENGINEInnoDB;insertintoleaf_alloc(biz_tag,max_id,step,description)values(leaf-segment-test,1,2000,Test leaf Segment Mode Get Id)之后在 pom.xml 里面因为这个项目比较老了所以我们包的依赖管理就单独在这个 biz 模块就可以了不放在父工程包依赖管理了propertiescommon-io.version2.4/common-io.versionperf4j.version0.9.16/perf4j.versiondruid.version1.0.18/druid.versionmybatis.version3.3.0/mybatis.versioncurator-recipes.version2.6.0/curator-recipes.versionzookeeper.version3.6.0/zookeeper.version/properties!-- 如果要服务注册feign 调用等自己加这里不多介绍 --dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependencydependencygroupIdcommons-io/groupIdartifactIdcommons-io/artifactIdversion${common-io.version}/version/dependencydependencygroupIdorg.perf4j/groupIdartifactIdperf4j/artifactIdversion${perf4j.version}/version/dependencydependencygroupIdcom.mysql/groupIdartifactIdmysql-connector-j/artifactId/dependencydependencygroupIdcom.alibaba/groupIdartifactIddruid/artifactIdversion${druid.version}/version/dependencydependencygroupIdorg.mybatis/groupIdartifactIdmybatis/artifactIdversion${mybatis.version}/version/dependency!-- zk --dependencygroupIdorg.apache.curator/groupIdartifactIdcurator-recipes/artifactIdversion${curator-recipes.version}/version!-- 为防止日志冲突添加以下排除项 --exclusionsexclusionartifactIdlog4j/artifactIdgroupIdlog4j/groupId/exclusionexclusionartifactIdorg.slf4j/artifactIdgroupIdslf4j-reload4j/groupId/exclusionexclusiongroupIdorg.slf4j/groupIdartifactIdslf4j-api/artifactId/exclusionexclusiongroupIdorg.apache.zookeeper/groupIdartifactIdzookeeper/artifactId/exclusion/exclusions/dependencydependencygroupIdorg.apache.zookeeper/groupIdartifactIdzookeeper/artifactIdversion${zookeeper.version}/version!-- 为防止日志冲突添加以下排除项 --exclusionsexclusiongroupIdorg.slf4j/groupIdartifactIdslf4j-api/artifactId/exclusionexclusiongroupIdorg.slf4j/groupIdartifactIdslf4j-log4j12/artifactId/exclusionexclusiongroupIdlog4j/groupIdartifactIdlog4j/artifactId/exclusion/exclusions/dependency还要在 /service 包下修改一下 SegmentService因为我用的是 mysql8.x如果你不是可以不用改如果你想为某个业务添加全局ID比如用户ID、帖子ID要在数据库里面执行一个插入SQLINSERTINTOleaf.leaf_alloc(biz_tag,max_id,step,description,update_time)VALUES(user-id,10100,2000,用户 ID,now());# 分别是业务唯一表示biz-tag# ID 起始值# 步长# 描述信息另外第二种雪花算法是基于 Zookeeper所以如果要用的话就得配置上leaf.nameidle-store leaf.segment.enabletrue leaf.jdbc.urljdbc:mysql://127.0.0.1:3306/leaf?useUnicodetruecharacterEncodingutf-8autoReconnecttrueuseSSLfalseserverTimezoneAsia/Shanghai leaf.jdbc.usernameroot leaf.jdbc.passwordHliu1026. leaf.snowflake.enabletrue leaf.snowflake.zk.address127.0.0.1:2181 leaf.snowflake.port2222默认两个都是关闭的用哪个就要配置哪个。