Leaf 分布式 ID 生成实战------美团 Leaf 从原理到落地
分库分表后自增主键废了、雪花算法机器号怎么分配、两步一个 ID 的数据库方式扛不住高并发------美团 Leaf 专治这些。号段模式用双 Buffer 预加载把 DB 查 ID 从两步优化到近零步,雪花模式用 ZooKeeper 自动分配 Worker ID 解决机器号冲突。本文从两种模式的原理讲起,到 DB 建表、部署配置。
为什么需要分布式 ID
单机 MySQL 时代,AUTO_INCREMENT 一把梭搞定。微服务分库分表后,一张表拆成 16 张分片------自增主键没法用了,因为 16 张表各自从 1 开始递增,全局一定出现重复 ID。
分布式 ID 的核心要求就两条:
- 全局唯一
- 高性能(每秒能生成几万到几十万个)
常用的方案:
| 方案 | 优点 | 缺点 |
|---|---|---|
| UUID | 简单,本地生成 | 太长、无序,B+Tree 索引不友好 |
| 自增主键 + 步长(DB 号段) | 有序、全局唯一 | 每次取号要查 DB,不够快 |
| Redis 自增 | 快 | Redis 持久化非强项,挂了丢数据 |
| 雪花算法 | 本地生成,极快 | 强依赖机器时钟,时钟回拨可能产生重复 ID |
美团 Leaf 把 DB 号段和雪花算法两个方案都收了进来,各取所长:
- 号段模式(Segment):DB 批量取号,服务端缓存 + 双 Buffer 预加载,DB 一跳抵一万次
- 雪花模式(Snowflake):纯本地生成,用 ZooKeeper 自动分配 Worker ID,解决时钟回拨
号段模式------怎么做到又快又唯一
思路
号段模式的核心就是:一次从 DB 取一批 ID,在内存里慢慢用,用完了再取下一批。
MySQL Leaf 服务 业务服务 MySQL Leaf 服务 业务服务 #mermaid-svg-Jjyp49X62OvNuUKm{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Jjyp49X62OvNuUKm .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Jjyp49X62OvNuUKm .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Jjyp49X62OvNuUKm .error-icon{fill:#552222;}#mermaid-svg-Jjyp49X62OvNuUKm .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Jjyp49X62OvNuUKm .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Jjyp49X62OvNuUKm .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Jjyp49X62OvNuUKm .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Jjyp49X62OvNuUKm .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Jjyp49X62OvNuUKm .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Jjyp49X62OvNuUKm .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Jjyp49X62OvNuUKm .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Jjyp49X62OvNuUKm .marker.cross{stroke:#333333;}#mermaid-svg-Jjyp49X62OvNuUKm svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Jjyp49X62OvNuUKm p{margin:0;}#mermaid-svg-Jjyp49X62OvNuUKm .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Jjyp49X62OvNuUKm text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-Jjyp49X62OvNuUKm .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Jjyp49X62OvNuUKm .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-Jjyp49X62OvNuUKm .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-Jjyp49X62OvNuUKm .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-Jjyp49X62OvNuUKm #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-Jjyp49X62OvNuUKm .sequenceNumber{fill:white;}#mermaid-svg-Jjyp49X62OvNuUKm #sequencenumber{fill:#333;}#mermaid-svg-Jjyp49X62OvNuUKm #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-Jjyp49X62OvNuUKm .messageText{fill:#333;stroke:none;}#mermaid-svg-Jjyp49X62OvNuUKm .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Jjyp49X62OvNuUKm .labelText,#mermaid-svg-Jjyp49X62OvNuUKm .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-Jjyp49X62OvNuUKm .loopText,#mermaid-svg-Jjyp49X62OvNuUKm .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-Jjyp49X62OvNuUKm .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Jjyp49X62OvNuUKm .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-Jjyp49X62OvNuUKm .noteText,#mermaid-svg-Jjyp49X62OvNuUKm .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-Jjyp49X62OvNuUKm .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Jjyp49X62OvNuUKm .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Jjyp49X62OvNuUKm .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Jjyp49X62OvNuUKm .actorPopupMenu{position:absolute;}#mermaid-svg-Jjyp49X62OvNuUKm .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-Jjyp49X62OvNuUKm .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Jjyp49X62OvNuUKm .actor-man circle,#mermaid-svg-Jjyp49X62OvNuUKm line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-Jjyp49X62OvNuUKm :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 这批号段:1001, 2000 服务端缓存 1000 个 ID alt 还有剩余 用完了 我要一个 ID(key=user) 内存里还有吗? 直接返回 ID UPDATE leaf_alloc SET max_id = max_id + step WHERE biz_tag = 'user' 新的 max_id=2000, step=1000 返回 ID = 1001
这样一来,取 1000 个 ID 才查一次 DB。step 越大,DB 压力越小,但万一 Leaf 服务挂了,没发出去的那批 ID 就丢了(不过问题不大,ID 不要求连续,跳号无所谓)。
双 Buffer------用的时候后台已经在准备下一批了
上面的方案还有一个问题:每次号段用完时,业务请求要等着 Leaf 去查 DB------这 10ms 左右的延迟对高并发场景来说还是有影响。
Leaf 的解法是双 Buffer:每个 key 维护两个号段,当前号段快用完(比如用了 90%)时,后台异步线程 去 DB 把下一个号段拉回来备着。当前号段用完后直接切到备用的,毫无停顿。
#mermaid-svg-WWBQHgYZ6lFMBRCl{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-WWBQHgYZ6lFMBRCl .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-WWBQHgYZ6lFMBRCl .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-WWBQHgYZ6lFMBRCl .error-icon{fill:#552222;}#mermaid-svg-WWBQHgYZ6lFMBRCl .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-WWBQHgYZ6lFMBRCl .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-WWBQHgYZ6lFMBRCl .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-WWBQHgYZ6lFMBRCl .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-WWBQHgYZ6lFMBRCl .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-WWBQHgYZ6lFMBRCl .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-WWBQHgYZ6lFMBRCl .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-WWBQHgYZ6lFMBRCl .marker{fill:#333333;stroke:#333333;}#mermaid-svg-WWBQHgYZ6lFMBRCl .marker.cross{stroke:#333333;}#mermaid-svg-WWBQHgYZ6lFMBRCl svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-WWBQHgYZ6lFMBRCl p{margin:0;}#mermaid-svg-WWBQHgYZ6lFMBRCl .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-WWBQHgYZ6lFMBRCl .cluster-label text{fill:#333;}#mermaid-svg-WWBQHgYZ6lFMBRCl .cluster-label span{color:#333;}#mermaid-svg-WWBQHgYZ6lFMBRCl .cluster-label span p{background-color:transparent;}#mermaid-svg-WWBQHgYZ6lFMBRCl .label text,#mermaid-svg-WWBQHgYZ6lFMBRCl span{fill:#333;color:#333;}#mermaid-svg-WWBQHgYZ6lFMBRCl .node rect,#mermaid-svg-WWBQHgYZ6lFMBRCl .node circle,#mermaid-svg-WWBQHgYZ6lFMBRCl .node ellipse,#mermaid-svg-WWBQHgYZ6lFMBRCl .node polygon,#mermaid-svg-WWBQHgYZ6lFMBRCl .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-WWBQHgYZ6lFMBRCl .rough-node .label text,#mermaid-svg-WWBQHgYZ6lFMBRCl .node .label text,#mermaid-svg-WWBQHgYZ6lFMBRCl .image-shape .label,#mermaid-svg-WWBQHgYZ6lFMBRCl .icon-shape .label{text-anchor:middle;}#mermaid-svg-WWBQHgYZ6lFMBRCl .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-WWBQHgYZ6lFMBRCl .rough-node .label,#mermaid-svg-WWBQHgYZ6lFMBRCl .node .label,#mermaid-svg-WWBQHgYZ6lFMBRCl .image-shape .label,#mermaid-svg-WWBQHgYZ6lFMBRCl .icon-shape .label{text-align:center;}#mermaid-svg-WWBQHgYZ6lFMBRCl .node.clickable{cursor:pointer;}#mermaid-svg-WWBQHgYZ6lFMBRCl .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-WWBQHgYZ6lFMBRCl .arrowheadPath{fill:#333333;}#mermaid-svg-WWBQHgYZ6lFMBRCl .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-WWBQHgYZ6lFMBRCl .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-WWBQHgYZ6lFMBRCl .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WWBQHgYZ6lFMBRCl .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-WWBQHgYZ6lFMBRCl .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WWBQHgYZ6lFMBRCl .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-WWBQHgYZ6lFMBRCl .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-WWBQHgYZ6lFMBRCl .cluster text{fill:#333;}#mermaid-svg-WWBQHgYZ6lFMBRCl .cluster span{color:#333;}#mermaid-svg-WWBQHgYZ6lFMBRCl div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-WWBQHgYZ6lFMBRCl .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-WWBQHgYZ6lFMBRCl rect.text{fill:none;stroke-width:0;}#mermaid-svg-WWBQHgYZ6lFMBRCl .icon-shape,#mermaid-svg-WWBQHgYZ6lFMBRCl .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WWBQHgYZ6lFMBRCl .icon-shape p,#mermaid-svg-WWBQHgYZ6lFMBRCl .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-WWBQHgYZ6lFMBRCl .icon-shape .label rect,#mermaid-svg-WWBQHgYZ6lFMBRCl .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WWBQHgYZ6lFMBRCl .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-WWBQHgYZ6lFMBRCl .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-WWBQHgYZ6lFMBRCl :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 内存中每个 biz_tag 的双 Buffer
value++ → 1501
没到 max=2000
Segment A
当前使用
value=1500, max=2000
Segment B
备用(已就绪)
value=2000, max=3000
请求来了,要 ID
返回 1501
A 用了 90% 时
后台线程去 DB 预加载
Segment B 提前就绪
DB 表结构
sql
CREATE DATABASE leaf;
USE leaf;
CREATE TABLE leaf_alloc (
biz_tag VARCHAR(128) NOT NULL DEFAULT '' COMMENT '业务标识,比如 user、order',
max_id BIGINT NOT NULL DEFAULT 1 COMMENT '当前已分配的最大 ID',
step INT NOT NULL DEFAULT 1000 COMMENT '每次分配的号段长度',
description VARCHAR(256) DEFAULT NULL COMMENT '业务描述',
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (biz_tag)
);
插入一条初始数据:
sql
INSERT INTO leaf_alloc(biz_tag, max_id, step, description)
VALUES ('user', 1, 1000, '用户表 ID');
号段模式的原理就这一条 SQL:
sql
-- 取号的时候:max_id = max_id + step,然后返回新的 max_id
UPDATE leaf_alloc SET max_id = max_id + step WHERE biz_tag = 'user';
-- 再查出来当前的 max_id 是多少
SELECT biz_tag, max_id, step FROM leaf_alloc WHERE biz_tag = 'user';
Leaf 服务拿到 max_id=1001,那就知道这批号段是 [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
序号 = 1
- 1 位:固定为 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。同一毫秒不同机器生成的 ID,Worker ID 不同,整个 ID 就不同:
服务器 A(Worker ID=1): 时间戳 + 0000000001 + 序号 → ID = xxx...1...001
服务器 B(Worker ID=2): 时间戳 + 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 的应对:
- 回拨不超过 5ms:sleep 等待,等时间追上之前的时间戳
- 回拨超过 5ms:直接拒绝生成,返回异常,不停留在"可能出错"的状态
- 用 ZooKeeper 维护 Worker ID:每次启动时去 ZK 注册,ZK 会分配一个全局唯一的 Worker ID。同时 Leaf 会往 ZK 周期性上报当前时间戳,如果重启后发现本地时间比 ZK 记录的值还小→抛出异常
ZooKeeper 怎么分配 Worker ID
ZooKeeper Leaf 实例(IP=192.168.1.5) ZooKeeper Leaf 实例(IP=192.168.1.5) #mermaid-svg-wFSJHmOxb1GJZTp6{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wFSJHmOxb1GJZTp6 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wFSJHmOxb1GJZTp6 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wFSJHmOxb1GJZTp6 .error-icon{fill:#552222;}#mermaid-svg-wFSJHmOxb1GJZTp6 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wFSJHmOxb1GJZTp6 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wFSJHmOxb1GJZTp6 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wFSJHmOxb1GJZTp6 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wFSJHmOxb1GJZTp6 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wFSJHmOxb1GJZTp6 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wFSJHmOxb1GJZTp6 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wFSJHmOxb1GJZTp6 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wFSJHmOxb1GJZTp6 .marker.cross{stroke:#333333;}#mermaid-svg-wFSJHmOxb1GJZTp6 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wFSJHmOxb1GJZTp6 p{margin:0;}#mermaid-svg-wFSJHmOxb1GJZTp6 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-wFSJHmOxb1GJZTp6 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-wFSJHmOxb1GJZTp6 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-wFSJHmOxb1GJZTp6 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-wFSJHmOxb1GJZTp6 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-wFSJHmOxb1GJZTp6 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-wFSJHmOxb1GJZTp6 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-wFSJHmOxb1GJZTp6 .sequenceNumber{fill:white;}#mermaid-svg-wFSJHmOxb1GJZTp6 #sequencenumber{fill:#333;}#mermaid-svg-wFSJHmOxb1GJZTp6 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-wFSJHmOxb1GJZTp6 .messageText{fill:#333;stroke:none;}#mermaid-svg-wFSJHmOxb1GJZTp6 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-wFSJHmOxb1GJZTp6 .labelText,#mermaid-svg-wFSJHmOxb1GJZTp6 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-wFSJHmOxb1GJZTp6 .loopText,#mermaid-svg-wFSJHmOxb1GJZTp6 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-wFSJHmOxb1GJZTp6 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-wFSJHmOxb1GJZTp6 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-wFSJHmOxb1GJZTp6 .noteText,#mermaid-svg-wFSJHmOxb1GJZTp6 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-wFSJHmOxb1GJZTp6 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-wFSJHmOxb1GJZTp6 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-wFSJHmOxb1GJZTp6 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-wFSJHmOxb1GJZTp6 .actorPopupMenu{position:absolute;}#mermaid-svg-wFSJHmOxb1GJZTp6 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-wFSJHmOxb1GJZTp6 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-wFSJHmOxb1GJZTp6 .actor-man circle,#mermaid-svg-wFSJHmOxb1GJZTp6 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-wFSJHmOxb1GJZTp6 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 节点编号 = 000000003 Worker 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 数据库的
sql
CREATE TABLE `leaf_alloc` (
`biz_tag` varchar(128) NOT NULL DEFAULT '',
`max_id` bigint(20) NOT NULL DEFAULT '1',
`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;
insert into leaf_alloc(biz_tag, max_id, step, description) values('leaf-segment-test', 1, 2000, 'Test leaf Segment Mode Get Id')
之后,在 pom.xml 里面,因为这个项目比较老了,所以我们包的依赖管理就单独在这个 biz 模块就可以了,不放在父工程包依赖管理了
xml
<properties>
<common-io.version>2.4</common-io.version>
<perf4j.version>0.9.16</perf4j.version>
<druid.version>1.0.18</druid.version>
<mybatis.version>3.3.0</mybatis.version>
<curator-recipes.version>2.6.0</curator-recipes.version>
<zookeeper.version>3.6.0</zookeeper.version>
</properties>
<!-- 如果要服务注册,feign 调用等自己加,这里不多介绍 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${common-io.version}</version>
</dependency>
<dependency>
<groupId>org.perf4j</groupId>
<artifactId>perf4j</artifactId>
<version>${perf4j.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>${mybatis.version}</version>
</dependency>
<!-- zk -->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>${curator-recipes.version}</version>
<!-- 为防止日志冲突,添加以下排除项 -->
<exclusions>
<exclusion>
<artifactId>log4j</artifactId>
<groupId>log4j</groupId>
</exclusion>
<exclusion>
<artifactId>org.slf4j</artifactId>
<groupId>slf4j-reload4j</groupId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>${zookeeper.version}</version>
<!-- 为防止日志冲突,添加以下排除项 -->
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
</exclusions>
</dependency>
还要在 /service 包下修改一下 SegmentService,因为我用的是 mysql8.x,如果你不是可以不用改

如果你想为某个业务添加全局ID,比如用户ID、帖子ID,要在数据库里面执行一个插入SQL
sql
INSERT INTO `leaf`.`leaf_alloc` (`biz_tag`, `max_id`, `step`, `description`, `update_time`) VALUES ('user-id', 10100, 2000, '用户 ID', now());
# 分别是业务唯一表示,biz-tag
# ID 起始值
# 步长
# 描述信息

另外,第二种雪花算法是基于 Zookeeper,所以如果要用的话,就得配置上
properties
leaf.name=idle-store
leaf.segment.enable=true
leaf.jdbc.url=jdbc:mysql://127.0.0.1:3306/leaf?useUnicode=true&characterEncoding=utf-8&autoReconnect=true&useSSL=false&serverTimezone=Asia/Shanghai
leaf.jdbc.username=root
leaf.jdbc.password=Hliu1026.
leaf.snowflake.enable=true
leaf.snowflake.zk.address=127.0.0.1:2181
leaf.snowflake.port=2222
默认两个都是关闭的,用哪个就要配置哪个。