尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Leaf 号段快取完时 DB 抖了 200ms,发号 RT 从 1ms 拉到 1.2s:雪花、号段、Leaf 的 3 个发号瓶颈
title: Leaf 号段快取完时 DB 抖了 200ms发号 RT 从 1ms 拉到 1.2s雪花、号段、Leaf 的 3 个发号瓶颈date: 2026-08-22category: 分布式IDtags: [Java, 后端, 分布式ID, 架构]我们的订单号、物流单号、支付流水号都来自一个统一的发号服务。早期用雪花算法上容器后踩了一脚 workerId 冲突的坑那次订单表主键报了 3000 次 Duplicate后来切到号段模式。但号段模式也不是银弹——有次 DB 慢查询把发号 RT 从 1ms 直接拉到 1.2s拖慢了整条下单链路。这篇把雪花、号段、Leaf 三种方案的真实瓶颈拆开尤其是号段模式的「双 buffer」到底解决什么、又留下什么坑。雪花容器化下 workerId 怎么来是第一个雷雪花 ID 由「时间戳 workerId 序列号」组成。单机时代 workerId 写死在配置里没问题容器化后 Pod 频繁重建、IP 漂移写死的 workerId 很容易撞车。// 雪花算法核心简化同一毫秒内靠 sequence 区分靠 workerId 区分机器 public synchronized long nextId() { long timestamp timeGen(); if (timestamp lastTimestamp) { throw new RuntimeException(时钟回拨); } if (timestamp lastTimestamp) { sequence (sequence 1) SEQUENCE_MASK; if (sequence 0) { timestamp tillNextMillis(lastTimestamp); // 本毫秒序号用尽等下一毫秒 } } else { sequence 0; } lastTimestamp timestamp; // 41 位时间戳 | 10 位 workerId | 12 位序列号 return (timestamp 22) | (workerId 12) | sequence; }踩坑点逐个说workerId必须全局唯一。我们用过「IP 末段取模 1024」生成结果两个 Pod 恰好拿到相同 IP 段workerId 撞了同一毫秒不同机器的 ID 完全相同订单表主键冲突。时钟回拨直接抛异常。K8s 节点 NTP 校准时偶尔回拨几百毫秒那几秒发号全失败。我们最后改成「回拨小于 5ms 就自旋等大于 5ms 才报警」而不是一刀切抛异常。序列号只有 12 位4096/毫秒高并发下单一个 worker 每秒上限约 409 万。对单实例够用但多实例聚合时被 workerId 位数10 位限制了机器规模。雪花还有一个常被忽略的软限制41 位时间戳大约能用 69 年从 1970 算起而且它依赖「相对某个 epoch 的偏移」。我们曾经有人把 epoch 设成了系统启动时间结果每次重启时间归零、ID 可能重复后来统一用2020-01-01作为固定 epoch 才稳住。号段模式不在 ID 里编码机器靠 DB 批量发号号段模式的思想完全不一样不去算 ID而是向 DB 申请一段区间比如 [1, 1000]内存里慢慢发发完再申请下一段。// 号段模式从 DB 取一段号本地自增发放 public class SegmentIdGenerator { private volatile long current; private volatile long max; // 当前段最大值 public synchronized long nextId() { if (current max) { // 当前段用完去 DB 取下一段并更新 max Segment seg fetchNextSegmentFromDb(); current seg.start; max seg.end; } return current; } }号段模式的好处是ID 里不带机器信息纯递增、可读、好排障。但瓶颈也明显每次「跨段」都要打一次 DB。如果 DB 慢跨段那一刻的 RT 直接挂到 DB 那边。我们那次 1.2s 的 RT就是因为号段临界点正好撞上 DB 一次慢查询。号段表的设计也很关键我们用的是类似 Leaf 的结构CREATE TABLE id_segment ( biz_tag VARCHAR(64) PRIMARY KEY, -- 业务标识如 order/pay/logistic max_id BIGINT NOT NULL, -- 当前已分配到的 ID step INT NOT NULL, -- 每次取的长度 version BIGINT NOT NULL -- 乐观锁防并发取段冲突 ); -- 取段用一句原子更新UPDATE id_segment SET max_id max_id step WHERE biz_tag?step就是「一次取多长」。step 太小跨段频繁DB 压力大step 太大服务重启会浪费一段 ID内存里没发完的段丢了。我们最后按业务峰值估算把 order 的 step 设到 10 万。Leaf 的双 buffer把「跨段卡顿」藏起来美团 Leaf 的号段模式做了个关键优化——双 buffer不等当前段用完才去取而是在用到一定比例比如 10%时就异步把下一段预取好。// Leaf 双 buffer 思路简化 class SegmentBuffer { Segment[] segments new Segment[2]; // 两个段一个在用一个备用 int currentPos 0; volatile boolean nextReady false; public long nextId() { Segment cur segments[currentPos]; long id cur.nextId(); // 用到 10% 且备用段还没在加载触发异步预取 if (cur.idleRatio() 0.1 !nextReady) { asyncLoadNextSegment(); } // 当前段用尽且备用段已就绪切换 if (cur.isOver() nextReady) { currentPos ^ 1; nextReady false; } return id; } }这个设计解决了号段模式的「临界卡顿」大部分请求命中内存里的段只有第一次取段和异步预取失败才会真的等 DB。但我们那次 DB 抖 200ms 时正好异步预取也跟着慢了buffer 切换的瞬间还是被拖了一下——双 buffer 降低的是「常规跨段」的延迟扛不住的是「DB 整体变慢」。Leaf 的高可用与号段监控号段模式把发号服务的可用性很大程度上交给了 DB所以高可用设计不能省。我们做了三件事号段表独立库 只读从库做预取源。主库只负责UPDATE ... SET max_id max_id step这一行原子更新预取读走从库主库压力极小。号段剩余告警。监控每个 biz_tag 的「当前 max_id - 已发放」比例低于 20% 就告警避免段耗尽又没及时取新段导致发号失败。多节点共享同一张段表 乐观锁 version。多个发号服务实例并发取段时靠UPDATE ... WHERE version ?防冲突谁更新成功谁拿到新段天然支持水平扩展。-- 取段是原子的CAS 式更新失败就重试 UPDATE id_segment SET max_id max_id 100000, version version 1 WHERE biz_tag order AND version 42;这套下来发号服务的 SLA 从「依赖单库」变成了「依赖 DB 主从 本地内存 buffer」单库抖动基本被双 buffer 吸收只有主库整体不可用才会影响发号。代价是架构多了一个要重点保障的 DB 组件——和雪花「依赖时钟」、Kafka「依赖 broker」一样没有哪种方案是零依赖的。另外补充一个细节号段模式重启会丢当前内存段里没发完的号所以 step 越大、重启浪费越多。我们按「单实例 1 分钟能发完的量」估 step既避免频繁跨段又把重启浪费控制在可接受范围。上线这套结构后半年发号服务经历了 3 次 DB 主库抖动、1 次从库延迟业务侧发号 RT 全程无感知SLA 维持在 99.99%这也反过来印证了「双 buffer 独立库」组合的有效性。三种方案对比看瓶颈在哪方案ID 是否递增依赖典型瓶颈雪花时间趋势递增机器时钟workerId 分配、时钟回拨号段严格递增DB 一段跨段打 DB 的 RTLeaf 双 buffer严格递增DB 一段 异步预取DB 整体变慢时预取延迟我的取舍如果你的 ID不需要连续递增、又跑在容器里别用裸雪花workerId 分配和时钟回拨够你喝一壶。要么上号段要么用「ZooKeeper/Redis 注册 workerId」的雪花变体。号段/Leaf 适合「ID 要可读、要递增、要好排查」的业务订单号、流水号但DB 必须稳。我们把号段表单独建库、配上只读从库做预取数据源DB 抖动对发号的影响降到了可忽略。号段有个常被忽略的点段要设得够大。我们早期一段只设 100结果高并发下每秒跨段几十次DB 压力反而比预期大调到 10 万一段后跨段频率降到几分钟一次DB 几乎没压力。复盘数字切到 Leaf 双 buffer 后发号 P99 从雪花的「时钟回拨时直接失败」变成稳定在 2ms 以内常规情况下跨段完全无感。那次 1.2s 的 RT 事故根因是号段表和业务库混在一个实例上业务高峰把 DB 连接占满预取 SQL 排队。把号段表迁到独立实例并设段大小 10 万后连续 3 个月发号 RT 全程低于 5msDB 慢查询零相关告警。上线半年发号服务支撑了日均 1.2 亿次调用零重复 ID。思考题号段模式 ID 严格递增这既是优点好排查也是缺点容易被竞争对手通过订单号推算你的日单量。如果你的业务既想要「不暴露规模」又想要「不依赖机器时钟」你会怎么改号段模式
RELATED

相关推荐

详解AUTOSAR:专栏总述

详解AUTOSAR:专栏总述

目录 1、理论篇 2、工具篇 3、环境配置篇 4、实践篇 5、项目篇 6、拓展篇 本专栏主要介绍汽车嵌入式系统软件规范AUTOSAR以及符合AUTOSAR规范的车用控制器软件开发方法。 在编写过程中尽可能以通俗易懂的语言和形象的图解来展现AUTOSAR中一些复杂的概念问题,…

📅 2026/10/11 12:25:12
spring boot 连接emqx并实现发布订阅

spring boot 连接emqx并实现发布订阅

1、安装依赖 <dependency><groupId>org.springframework.integration</groupId><artifactId>spring-integration-mqtt</artifactId><version>7.1.0</version><scope>compile</scope></dependency><!-- Source:…

📅 2026/10/11 16:12:44
运维手动改了一行数据,Seata 回滚时把它覆盖了:AT 模式全局锁和 undo_log 的 3 个被忽略的边界

运维手动改了一行数据,Seata 回滚时把它覆盖了:AT 模式全局锁和 undo_log 的 3 个被忽略的边界

title: 运维手动改了一行数据&#xff0c;Seata 回滚时把它覆盖了&#xff1a;AT 模式全局锁和 undo_log 的 3 个被忽略的边界 date: 2026-08-22 category: 分布式事务 tags: [Java, 后端, 分布式事务, Seata, 微服务]我们订单和库存两个服务用 Seata AT 模式做分布式事务。AT …

📅 2026/10/14 13:43:15
MORE NEWS

更多资讯

📰

电工作业刀上岗必备,别被中介坑了!3分钟搞懂报考全流程

电工作业刀上岗必备,别被中介坑了!3分钟搞懂报考全流程 你是不是也遇到过这种情况?手里拿着电工作业刀,却不知道去哪报名?怕被中介骗钱?别急,今天我来帮你理清楚电工作业刀上岗必备的全流程,告诉你怎么避开坑,顺利拿到证。…

📰

特种工式查询多久拿证?别怕考不过白交培训费,这几点你必须知道

特种工式查询多久拿证?别怕考不过白交培训费,这几点你必须知道 怕考不过白交培训费?特种作业证考试确实让人有点忐忑,尤其是你还在纠结 多久拿证…

📰

林州低压电工证官方回应:实操考试心里没底怕挂科?这几点必须知道

林州低压电工证官方回应:实操考试心里没底怕挂科?这几点必须知道 你是不是也经常在想,考林州低压电工证,实操考试心里没底怕挂科?其实,这个问题官方回应过,也给出了明确的备考路径。别急,下面的内容,都是基于河南省应急管理厅的最新政策和培训机构的…

📰

电工没有电工证归谁管?在哪考?劳务班组负责人必看!

电工没有电工证归谁管?在哪考?劳务班组负责人必看! 你正愁找不到电工岗位?电工证没考下来,连面试资格都没有,这事儿真让人头疼。信阳浉河区劳务班组负责人王师傅就遇到过,因为电工没证,项目方直接拒绝了他们班组。电工证这事,…

📰

特种架子工好考吗多久拿证郑州转岗从业者全攻略

特种架子工好考吗多久拿证郑州转岗从业者全攻略 之前考的证在外省能不能转过来?这个问题我听得太多了,我自己也经历过从河南郑州转岗到山东,拿着老家的特种架子工证在新地方干活被拦下,差点耽误了工期。特种架子工好考吗?多久拿证?这些其实都是新手最容…

📰

电工证去哪干活好呢报名费多少钱

电工证过期了不知道怎么复审延期?通过率揭秘让你少走弯路 证书过期了不知道怎么复审延期,这事儿不少电工都遇到过。尤其是像河南这类建筑工人密集的地方,电工证作为从业门槛之一,一旦失效,不仅影响工作,还可能面临法律风险。很多人问:…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬