Leaf 分布式 ID 生成实战——美团 Leaf 从原理到落地

Leaf 分布式 ID 生成实战------美团 Leaf 从原理到落地

分库分表后自增主键废了、雪花算法机器号怎么分配、两步一个 ID 的数据库方式扛不住高并发------美团 Leaf 专治这些。号段模式用双 Buffer 预加载把 DB 查 ID 从两步优化到近零步,雪花模式用 ZooKeeper 自动分配 Worker ID 解决机器号冲突。本文从两种模式的原理讲起,到 DB 建表、部署配置。


为什么需要分布式 ID

单机 MySQL 时代,AUTO_INCREMENT 一把梭搞定。微服务分库分表后,一张表拆成 16 张分片------自增主键没法用了,因为 16 张表各自从 1 开始递增,全局一定出现重复 ID。

分布式 ID 的核心要求就两条:

  1. 全局唯一
  2. 高性能(每秒能生成几万到几十万个)

常用的方案:

方案 优点 缺点
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 的应对:

  1. 回拨不超过 5ms:sleep 等待,等时间追上之前的时间戳
  2. 回拨超过 5ms:直接拒绝生成,返回异常,不停留在"可能出错"的状态
  3. 用 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

默认两个都是关闭的,用哪个就要配置哪个。

相关推荐
2603_954708311 小时前
全维度容错设计,打造微电网安全运行屏障
服务器·网络·数据库·人工智能·分布式·安全
国服第二切图仔2 小时前
HarmonyOS APP《画伴梦工厂》开发第60篇-分布式软总线2.0——多设备协同新范式
分布式·wpf·harmonyos
ZhengEnCi2 小时前
S02-SpringBoot实体类新增字段对已有数据的影响及自动DDL同步机制详解
数据库·spring boot
程序员张32 小时前
SpringBoot集成BCrypt密码加密库
java·spring boot·后端
闲猫2 小时前
Spring AI Agentic 模式(第1部分):Agent Skills——模块化、可复用的能力
java·人工智能·spring
源图客2 小时前
云途物流API开发-鉴权认证(Java)
java·网络·python
liguojun20253 小时前
篮球馆自动计时收费系统:从规则配置到自动结算的全流程拆解
java·大数据·运维·人工智能·物联网·1024程序员节
952363 小时前
RabbitMQ-基础操作
java·spring boot·分布式·后端·spring·rabbitmq
卓怡学长4 小时前
w266基于spring boot + vue 圣地延安美食乐享系统
java·数据库·vue.js·spring boot·spring·intellij-idea
C++、Java和Python的菜鸟4 小时前
第7章 Java高级技术
java·开发语言