1. 从一个“看起来很简单”的需求说起分布式系统里生成一个不重复的 ID这件事听起来像是入门级需求但真正在线上跑过高并发业务的人都清楚它是个不折不扣的深水区。我最早接触这个问题是在一个订单系统里当时用的是数据库自增主键单库单表跑得好好的日订单量也就几十万。后来业务扩张订单表要分库分表问题立刻暴露出来两个库各自从 1 开始自增订单号直接撞车用户看到两个不同的订单拥有同一个编号客服系统里查出来的记录张冠李戴。那次事故之后我开始系统性地研究全局唯一 ID 的各种方案踩过的坑、算过的账、半夜被叫起来排查的重复 ID 问题加起来能写一本小册子。这篇文章就是把这些年关于全局唯一 ID 的实战经验整理出来。核心关键词是全局唯一ID、分布式ID生成、高并发ID方案、雪花算法、号段模式。我会从方案选型的底层逻辑讲起把每种方案的适用场景、性能边界、坑点都拆开揉碎再给出可以直接参考的实操配置和代码骨架。不管你是刚接触分布式系统的新手还是正在为 ID 生成方案头疼的资深开发应该都能从中找到能直接用的东西。先说清楚这个问题的本质全局唯一 ID 要解决的不是“怎么生成一个不重复的数字”而是“在分布式环境下多个节点同时生成 ID 时如何保证不重复、趋势递增、性能足够高、且不依赖单点”。这四个约束条件互相拉扯没有一种方案能同时完美满足所有要求所以选型的本质是在这些约束之间做取舍。2. 全局唯一ID的核心需求与方案选型逻辑2.1 一个合格的全局唯一ID到底要满足什么很多人一上来就问“用雪花算法还是号段模式”但我觉得应该先退一步把需求理清楚。根据我实际踩坑的经验一个合格的全局唯一 ID 至少要满足以下几个维度的要求而且不同业务对这些维度的优先级完全不同。唯一性是底线。这个不用多说ID 重复意味着数据错乱在订单、支付、账务这类场景里是灾难性的。但唯一性的实现方式有区别是严格全局唯一还是业务维度唯一比如订单号在订单系统内唯一就够了不需要和用户 ID 共享一个全局空间。趋势递增。这个需求经常被低估。为什么 ID 要趋势递增因为大多数数据库的索引结构是 B 树如果 ID 是完全随机的每次插入都会导致页分裂和随机 IO写入性能会断崖式下跌。我实测过在 MySQL InnoDB 里用 UUID 做主键的写入吞吐量大概只有自增 ID 的三分之一到五分之一数据量越大差距越明显。所以趋势递增不是为了好看是实打实的性能需求。高性能与低延迟。高并发场景下ID 生成往往是关键路径上的操作。如果生成一个 ID 要花 10 毫秒那单机 QPS 上限就被锁死在 100 左右再多的机器也救不了。一般来说本地生成的方案能做到微秒级依赖远程调用的方案要做到毫秒级以内。高可用。ID 生成服务挂了整个业务就写不进去了。所以方案里不能有单点或者单点故障后要有快速降级和恢复机制。信息安全。有些业务场景下ID 是暴露给用户的比如订单号。如果订单号是连续的竞争对手下两单就能算出你一天的订单量。所以这类场景需要 ID 不可预测或者至少不能轻易推算。可读性与业务含义。有些团队喜欢在 ID 里嵌入时间、业务类型、分片信息方便排查问题。但这会增加复杂度需要权衡。下面这张表是我总结的不同业务场景对 ID 需求的优先级排序选型时可以先对号入座业务场景唯一性趋势递增性能要求信息安全典型方案订单号严格中等极高高雪花算法混淆用户ID严格低高中号段模式日志追踪ID严格低极高低UUID/雪花账务流水号严格高高高数据库自增步长消息ID严格低极高低雪花算法2.2 为什么数据库自增主键在分布式下会失效在单库单表时代AUTO_INCREMENT是最省心的方案性能好、趋势递增、实现简单。但一旦分库分表它的局限性就暴露无遗。最直接的问题是跨库冲突。两个库各自维护自己的自增序列生成的 ID 必然重复。有人会说那我把步长设成库的数量不就行了比如两个库一个从 1 开始步长 2一个从 2 开始步长 2。这个方案确实能解决冲突但它有几个致命缺陷。第一扩容困难。假设你一开始规划了 2 个库步长设为 2。后来业务增长要扩到 4 个库步长要改成 4但已经生成的 ID 序列和新的步长对不上需要停机迁移数据。我见过一个团队因为这个问题扩容时整整停服了 6 个小时。第二性能瓶颈。所有 ID 生成都要写数据库数据库的写入能力是有上限的。单库 MySQL 在优化得当的情况下写入 QPS 大概在几千到一万左右再高就要靠分库。但 ID 生成本身又需要全局协调这就形成了一个矛盾。第三依赖数据库可用性。数据库挂了ID 就生成不了业务直接停摆。虽然可以用主从切换但切换期间仍然有不可用窗口。所以数据库自增方案只适合小规模、对可用性要求不极端的场景。一旦上了规模就必须换方案。2.3 主流方案的全景对比在深入每种方案之前先用一张表把主流方案的核心指标拉通对比这样你心里有个全局图景方案唯一性保证趋势递增性能单机QPS可用性信息安全实现复杂度数据库自增单库唯一强数千低差低数据库步长全局唯一强数千低差低UUID全局唯一无极高高好极低雪花算法全局唯一强百万级高中中号段模式全局唯一强十万级中中中Redis自增全局唯一强十万级中中低美团Leaf全局唯一强百万级高中高这张表里的性能数据是我在 4 核 8G 的机器上实测的粗略值实际会因配置和实现细节有差异。可以看到没有一种方案在所有维度上都占优。雪花算法在性能和趋势递增上表现最好但依赖时钟号段模式性能不错且不依赖时钟但需要数据库配合UUID 性能最高但完全没有趋势递增。选型的核心逻辑是先确定你的业务对哪个维度最敏感然后在这个维度上找最优解同时确保其他维度不踩红线。比如订单系统对信息安全敏感那就要在雪花算法基础上做混淆日志系统对性能敏感但对信息安全不敏感UUID 就够用。3. 雪花算法高性能场景的首选但时钟是命门3.1 雪花算法的结构拆解与参数计算雪花算法的核心思想是把一个 64 位的长整型拆成几段每段代表不同的含义拼在一起就保证了全局唯一。标准的雪花算法结构是这样的1 位符号位固定为 0保证 ID 为正数41 位时间戳单位是毫秒从某个起始时间开始算10 位工作机器 ID可以拆成 5 位数据中心 ID 5 位机器 ID12 位序列号同一毫秒内的自增计数这个结构不是随便定的每一段的位数都是算出来的。41 位时间戳能表示 2 的 41 次方毫秒换算成年是约 69.7 年。也就是说从你设定的起始时间开始这个算法能用将近 70 年。如果你的系统生命周期没这么长可以适当减少时间戳位数把省下来的位数给机器 ID 或序列号。10 位机器 ID 意味着最多支持 1024 个节点。如果你的集群规模超过这个数要么扩展机器 ID 位数要么用其他方案。12 位序列号意味着同一毫秒内每个节点最多生成 4096 个 ID也就是单节点理论 QPS 上限是 4096 * 1000 409.6 万。这个数字看起来很夸张但实际受限于系统调用和网络开销单机跑到百万级已经很不错了。我实际用过的配置里比较稳妥的是把机器 ID 扩展到 12 位支持 4096 个节点序列号保持 12 位时间戳保持 41 位这样总位数还是 64 位但节点容量翻了两番。代价是时间戳的起始点要往后挪可用年限会缩短但对于大多数业务来说30 年足够了。3.2 时钟回拨雪花算法最大的坑雪花算法最让人头疼的问题就是时钟回拨。什么是时钟回拨简单说就是服务器的时间突然往回跳了。原因可能是 NTP 同步、闰秒调整、虚拟机迁移甚至人为修改系统时间。时钟回拨为什么致命因为雪花算法依赖时间戳递增来保证 ID 递增。如果时间回拨了新生成的时间戳可能比之前的小那生成的 ID 就可能和之前的重复。我遇到过最严重的一次某台机器的时钟回拨了 3 秒导致那 3 秒内生成的 ID 全部和之前重复数据库主键冲突写入全部失败。处理时钟回拨有几种常见策略我按推荐程度排序策略一小回拨等待大回拨报警。如果回拨时间在可接受范围内比如 5 毫秒以内就自旋等待直到时间追上上次生成的时间戳。如果超过阈值直接抛异常并报警让运维介入。这是最稳妥的做法因为大回拨往往意味着系统出了问题强行生成 ID 可能掩盖更大的故障。策略二备用机器 ID。每台机器预留几个备用机器 ID检测到时钟回拨时切换到备用 ID 继续生成。这样 ID 不会重复但会破坏趋势递增。适合对递增要求不严格的场景。策略三使用逻辑时钟。不依赖系统时间而是维护一个单调递增的逻辑时钟。每次生成 ID 时逻辑时钟加一而不是读系统时间。这样彻底避免了时钟回拨但逻辑时钟的持久化和恢复是个问题重启后要保证逻辑时钟不回退。我个人的经验是策略一配合完善的监控是最实用的。在代码里加一个时钟回拨检测回拨超过 5 毫秒就记录详细日志并触发告警同时拒绝生成 ID。这样虽然会短暂影响可用性但避免了数据错乱两害相权取其轻。3.3 雪花算法的代码骨架与配置要点下面是一个经过生产验证的雪花算法实现骨架用 Java 写的关键地方我都加了注释public class SnowflakeIdGenerator { // 起始时间戳2024-01-01 00:00:00 private static final long START_TIMESTAMP 1704038400000L; // 各部分位数 private static final long SEQUENCE_BITS 12L; private static final long WORKER_ID_BITS 10L; // 最大值 private static final long MAX_SEQUENCE ~(-1L SEQUENCE_BITS); private static final long MAX_WORKER_ID ~(-1L WORKER_ID_BITS); // 位移量 private static final long WORKER_ID_SHIFT SEQUENCE_BITS; private static final long TIMESTAMP_SHIFT SEQUENCE_BITS WORKER_ID_BITS; private final long workerId; private long sequence 0L; private long lastTimestamp -1L; // 时钟回拨容忍阈值单位毫秒 private static final long MAX_BACKWARD_MS 5L; public SnowflakeIdGenerator(long workerId) { if (workerId 0 || workerId MAX_WORKER_ID) { throw new IllegalArgumentException(workerId out of range); } this.workerId workerId; } public synchronized long nextId() { long currentTimestamp System.currentTimeMillis(); // 时钟回拨检测 if (currentTimestamp lastTimestamp) { long offset lastTimestamp - currentTimestamp; if (offset MAX_BACKWARD_MS) { // 小回拨等待 try { Thread.sleep(offset 1); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } currentTimestamp System.currentTimeMillis(); if (currentTimestamp lastTimestamp) { throw new RuntimeException(Clock moved backwards, refusing to generate id); } } else { throw new RuntimeException(Clock moved backwards too much: offset ms); } } if (currentTimestamp lastTimestamp) { sequence (sequence 1) MAX_SEQUENCE; if (sequence 0) { // 当前毫秒序列号用尽等待下一毫秒 currentTimestamp waitNextMillis(lastTimestamp); } } else { sequence 0L; } lastTimestamp currentTimestamp; return ((currentTimestamp - START_TIMESTAMP) TIMESTAMP_SHIFT) | (workerId WORKER_ID_SHIFT) | sequence; } private long waitNextMillis(long lastTimestamp) { long timestamp System.currentTimeMillis(); while (timestamp lastTimestamp) { timestamp System.currentTimeMillis(); } return timestamp; } }这段代码有几个关键点需要注意。nextId方法加了synchronized因为序列号是共享状态多线程并发时必须加锁。如果对性能有极致要求可以用ThreadLocal或者 CAS 来优化但大多数场景下synchronized的开销可以接受。waitNextMillis里的自旋等待是必要的因为同一毫秒内序列号用尽后必须等到下一毫秒才能继续生成。这个自旋时间通常很短不会成为瓶颈。起始时间戳的选择有讲究。不要用 1970 年因为那样时间戳部分会很快用完。也不要用当前时间因为一旦设定就不能改改了会导致 ID 重复。一般选一个最近的、有纪念意义的日期比如系统上线日期。3.4 雪花算法的部署与运维经验雪花算法部署时最大的问题是机器 ID 的分配。1024 个节点怎么保证每个节点的 ID 不重复我见过几种做法各有优劣。手动配置是最简单的在配置文件里写死。但集群规模大了之后手动分配容易出错而且扩容时要改配置重启。基于 IP 自动计算是常见做法取 IP 地址的后几位做机器 ID。但问题是容器环境下 IP 会变而且不同网段的 IP 后几位可能冲突。基于注册中心分配是最可靠的服务启动时向注册中心申请一个机器 ID注册中心保证不重复。但引入了额外依赖注册中心挂了服务就起不来。我实际用过的方案是基于 ZooKeeper 或 etcd 的临时顺序节点分配机器 ID。服务启动时在指定路径下创建一个临时顺序节点节点序号就是机器 ID。服务下线后节点自动删除ID 可以被复用。这个方案的好处是自动化、无冲突、支持动态扩缩容。缺点是依赖外部组件但 ZooKeeper 和 etcd 本身就是高可用组件风险可控。还有一个容易被忽略的点时钟同步。雪花算法依赖系统时间如果集群内机器时间不一致生成的 ID 可能乱序。所以必须配置 NTP 同步并且监控各机器的时间偏差。我一般会把时间偏差告警阈值设在 10 毫秒超过就排查。4. 号段模式不依赖时钟的稳健选择4.1 号段模式的核心原理与优势号段模式Segment的思路和雪花算法完全不同。它不依赖时间戳而是从数据库批量获取一段 ID 区间然后在内存里分配。比如一次从数据库取 1000 个 ID内存里慢慢发发完了再取下一段。这个方案的核心优势是不依赖时钟所以没有时钟回拨问题。同时性能很高因为大部分 ID 生成都在内存里完成只有取号段时才访问数据库。假设一次取 1000 个号段数据库 QPS 是 1000那理论 ID 生成 QPS 就是 100 万。号段模式的另一个好处是趋势递增。因为号段是顺序获取的内存里也是顺序分配的所以生成的 ID 整体趋势递增。虽然不同节点之间可能有交叉但整体方向是递增的对数据库索引友好。但号段模式也有缺点。ID 不连续因为每个节点预取了一段如果节点重启未用完的号段就浪费了导致 ID 出现空洞。这在某些对连续性有要求的场景比如发票号可能不可接受。依赖数据库虽然访问频率低但数据库挂了仍然会影响号段获取需要有降级策略。4.2 号段模式的数据库设计与参数计算号段模式的数据库表设计很简单核心就是记录每个业务键当前的最大 IDCREATE TABLE id_segment ( biz_tag VARCHAR(64) NOT NULL COMMENT 业务标识, max_id BIGINT NOT NULL DEFAULT 0 COMMENT 当前最大ID, step INT NOT NULL DEFAULT 1000 COMMENT 号段步长, update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (biz_tag) ) ENGINEInnoDB;取号段的 SQL 是原子操作UPDATE id_segment SET max_id max_id step WHERE biz_tag order RETURNING max_id;注意这里用了RETURNINGMySQL 不支持这个语法需要用事务包起来BEGIN; SELECT max_id FROM id_segment WHERE biz_tag order FOR UPDATE; UPDATE id_segment SET max_id max_id step WHERE biz_tag order; COMMIT;步长step的设置是个权衡。步长太小数据库访问频繁性能上不去步长太大ID 空洞多而且节点重启后浪费的 ID 多。我一般会根据业务 QPS 来算假设业务峰值 QPS 是 10 万希望号段能撑 10 分钟那步长就是 10 万 * 600 6000 万。这个数字太大了实际不会这么设。更合理的做法是让号段撑几秒到几十秒步长设在几千到几万之间。还有一个优化点双 Buffer。意思是内存里维护两个号段当前号段用到一定程度比如 10%时异步去数据库取下一个号段。这样取号段的耗时被隐藏了ID 生成几乎无延迟。美团 Leaf 就是这么做的效果很好。4.3 号段模式的容灾与降级策略号段模式最大的风险是数据库不可用。如果数据库挂了新的号段取不到内存里的号段用完后 ID 就生成不了了。所以必须有容灾策略。策略一本地缓存备用号段。服务启动时除了取当前号段再额外取一个备用号段存在本地文件里。数据库不可用时切换到备用号段。备用号段的步长可以设大一些比如正常步长的 10 倍这样能撑更久。策略二多数据库实例。号段表可以部署在主从架构上主库挂了切从库。但要注意从库有延迟切换时可能取到旧的 max_id导致 ID 重复。所以切换前要确保从库已经同步到最新数据。策略三降级到雪花算法。如果数据库完全不可用可以临时降级到雪花算法生成 ID。但这样生成的 ID 和号段模式的 ID 格式不同需要业务能兼容。而且雪花算法依赖时钟降级期间要确保时钟正常。我实际用过的方案是策略一加策略二的组合本地缓存备用号段应对短暂故障数据库主从切换应对长时间故障。同时监控号段余量余量低于阈值就告警提前发现数据库问题。4.4 号段模式的代码实现与踩坑记录下面是一个号段模式的核心实现包含了双 Buffer 和本地缓存public class SegmentIdGenerator { private final String bizTag; private final SegmentDao segmentDao; // 当前号段 private volatile Segment current; // 下一个号段 private volatile Segment next; // 是否正在加载下一个号段 private volatile boolean loading false; // 本地备用号段文件路径 private static final String BACKUP_FILE /data/id_backup/; public synchronized long nextId() { if (current null) { loadCurrentSegment(); } // 当前号段用到 10% 时异步加载下一个 if (!loading current.usage() 0.1) { loading true; asyncLoadNext(); } if (current.isExhausted()) { // 当前号段用尽切换到下一个 if (next ! null) { current next; next null; loading false; } else { // 下一个还没加载好同步加载 loadCurrentSegment(); } } return current.nextId(); } private void loadCurrentSegment() { try { Segment segment segmentDao.fetchSegment(bizTag); this.current segment; // 同时写入本地备份 writeBackup(segment); } catch (Exception e) { // 数据库不可用尝试从本地备份恢复 Segment backup readBackup(); if (backup ! null) { this.current backup; } else { throw new RuntimeException(Cannot load segment, e); } } } private void asyncLoadNext() { new Thread(() - { try { next segmentDao.fetchSegment(bizTag); } catch (Exception e) { // 加载失败重置标志位下次重试 loading false; } }).start(); } }这段代码有几个坑点。第一nextId方法加了synchronized因为号段切换是状态变更必须串行。如果对性能有要求可以用AtomicLong做无锁化但逻辑会复杂很多。第二异步加载下一个号段时如果加载失败要重置loading标志位否则后续不会再触发加载。这个细节很容易漏掉导致号段用尽后卡死。第三本地备份的写入和读取要考虑并发和文件损坏。我一般用临时文件加原子重命名的方式写入读取时校验文件完整性。还有一个实际踩过的坑号段步长和数据库自增的冲突。如果号段表的主键是自增的而max_id也是自增的两个自增序列会互相干扰。所以号段表的主键不要用自增用biz_tag做主键就够了。5. 其他方案与混合策略5.1 Redis自增方案的适用边界Redis 的INCR命令是原子操作天然适合生成全局唯一 ID。性能上单机 Redis 能跑到 10 万 QPS 以上配合集群能更高。实现也简单几行代码就能搞定。但 Redis 方案有几个硬伤。持久化问题Redis 重启后如果没开持久化或者持久化丢失INCR的起始值会回退导致 ID 重复。所以必须开 AOF 持久化并且设置appendfsync always但这会严重降低性能。单点问题Redis 主从切换时如果主库的INCR值还没同步到从库切换后新主库的起始值可能偏小导致 ID 重复。内存成本虽然每个 key 占用的内存很小但如果有大量业务键累积起来也不少。所以 Redis 方案适合对 ID 连续性要求不高、能容忍少量重复风险、且已有 Redis 基础设施的场景。比如日志追踪 ID、临时会话 ID 这类。对于订单、支付这类核心业务我不推荐用 Redis 生成 ID。5.2 UUID与类UUID方案的取舍UUID 最大的优点是本地生成、性能极高、完全不依赖外部组件。标准 UUID 是 128 位通常用 36 个字符表示比如550e8400-e29b-41d4-a716-446655440000。但 UUID 做数据库主键是灾难。前面说过随机 ID 会导致 B 树索引频繁页分裂写入性能极差。而且 36 个字符的存储空间比 8 字节的 long 大很多索引体积膨胀查询也慢。如果一定要用 UUID可以考虑有序 UUID比如 ULID 或者自己改造的版本。ULID 的前 48 位是时间戳后 80 位是随机数整体趋势递增同时保持本地生成的高性能。但 ULID 仍然是 128 位存储成本还是比 long 高。我的建议是能用 long 就用 long实在需要本地生成且不依赖外部组件用雪花算法而不是 UUID。雪花算法生成的也是 long趋势递增性能也好唯一的问题是时钟回拨但这个问题有成熟的解决方案。5.3 混合方案取长补短的工程实践实际生产中很少有系统只用一种 ID 生成方案。更常见的是混合方案根据不同业务场景选择不同策略。比如一个电商系统订单号用雪花算法加混淆保证唯一性和信息安全用户 ID 用号段模式因为用户注册频率不高号段模式足够且不依赖时钟日志追踪 ID 用 UUID因为日志量大且对性能要求极高UUID 的本地生成特性最合适。还有一种混合思路是雪花算法加号段模式用号段模式生成机器 ID解决雪花算法机器 ID 分配的问题同时用雪花算法生成实际 ID解决号段模式依赖数据库的问题。这样两种方案互相补位整体可用性更高。我实际做过的一个方案是主用雪花算法降级用号段模式。正常情况下雪花算法生成 ID性能高且趋势递增。检测到时钟回拨超过阈值时自动降级到号段模式避免 ID 重复。号段模式的数据库访问频率很低对数据库压力小作为降级方案很合适。6. 常见问题排查与避坑指南6.1 ID重复问题的排查思路ID 重复是最严重的问题一旦发现要立即排查。我整理了一个排查流程按可能性从高到低排列第一步检查时钟回拨。如果是雪花算法先看日志里有没有时钟回拨的记录。用ntpq -p或者chronyc tracking查看时间同步状态。如果发现回拨基本可以定位问题。第二步检查机器 ID 冲突。如果是雪花算法确认是否有两个节点用了同一个机器 ID。检查配置文件和注册中心的分配记录。容器环境下特别容易出这个问题因为 IP 可能重复。第三步检查号段重复。如果是号段模式确认是否有两个节点取了同一个号段。检查数据库的max_id更新是否原子事务隔离级别是否正确。主从切换时特别容易出这个问题。第四步检查序列号溢出。雪花算法的序列号是 12 位同一毫秒最多 4096 个。如果单节点 QPS 超过 4096 * 1000序列号会溢出导致等待下一毫秒。如果等待逻辑有 bug可能生成重复 ID。第五步检查并发问题。确认 ID 生成器的线程安全性。synchronized是否覆盖了所有共享状态有没有用ThreadLocal但忘记清理这些细节都可能导致重复。下面这张表是我总结的 ID 重复问题速查表现象可能原因排查方法解决方案偶发重复时钟回拨检查 NTP 日志加时钟回拨检测批量重复机器ID冲突检查节点配置用注册中心分配切换后重复主从延迟检查同步状态切换前确保同步高并发重复序列号溢出压测复现优化等待逻辑重启后重复状态未持久化检查持久化配置加本地备份6.2 性能瓶颈的定位与优化ID 生成成为性能瓶颈时通常表现为生成耗时增加、QPS 上不去、或者 CPU 飙高。定位思路如下。先看是本地生成还是远程调用。雪花算法是本地生成理论上微秒级。如果耗时超过毫秒说明有锁竞争或者自旋等待。号段模式大部分是本地生成偶尔远程调用。如果频繁远程调用说明步长设小了。再看锁竞争。雪花算法的synchronized在高并发下会成为瓶颈。可以用LongAdder或者分段锁优化。号段模式的号段切换也有锁但频率低影响小。然后看数据库。号段模式的数据库访问是瓶颈点。优化方向是加大步长、用双 Buffer 隐藏延迟、读写分离。如果数据库 QPS 已经很高考虑分库或者换方案。最后看网络。如果 ID 生成是远程服务网络延迟是主要开销。优化方向是本地缓存号段、批量获取、或者改成 SDK 本地生成。我实测过的一个优化案例某系统用号段模式步长 1000QPS 5 万时数据库压力很大。把步长改成 10 万加双 Buffer数据库 QPS 降到个位数ID 生成 QPS 提升到 50 万效果立竿见影。6.3 生产环境部署的注意事项监控先行。ID 生成服务必须有完善的监控生成 QPS、耗时 P99、时钟偏差、号段余量、数据库连接数。这些指标要接入告警异常时第一时间知道。容量规划。根据业务峰值 QPS 和增长趋势提前规划 ID 生成能力。雪花算法的容量上限是 409.6 万 QPS 每节点号段模式取决于步长和数据库能力。留足余量至少 3 倍峰值。灰度发布。ID 生成逻辑变更影响面大必须灰度。先在小流量环境验证确认无重复、性能达标后再全量。回滚预案。准备好回滚方案一旦新方案出问题能快速切回旧方案。回滚时要注意 ID 格式兼容性避免新旧 ID 混用导致问题。压测验证。上线前必须压测模拟峰值流量验证 ID 生成能力和唯一性。压测时要多节点同时跑检查是否有冲突。我个人还有一个习惯在 ID 生成器里加一个自检逻辑。每隔一段时间生成一批 ID检查是否有重复结果上报监控。这样即使有偶发问题也能及时发现。6.4 一个真实的故障复盘最后分享一个我亲历的故障。某次大促订单系统突然大量报主键冲突。排查发现是雪花算法的时钟回拨导致的。具体原因是运维在扩容时新节点的 NTP 配置有问题时间比标准时间慢了 2 秒。新节点启动后生成的 ID 时间戳部分比老节点小导致 ID 重复。故障的直接原因是 NTP 配置错误但根本原因是缺少时钟回拨检测和告警。如果代码里有回拨检测新节点启动时就会拒绝生成 ID 并报警不会造成数据错乱。修复方案有三步第一紧急下线问题节点恢复 NTP 配置第二在代码里加时钟回拨检测回拨超过 5 毫秒就拒绝生成并告警第三在部署流程里加 NTP 检查时间偏差超过阈值不允许上线。这个故障给我的教训是ID 生成方案的技术选型只是第一步配套的监控、告警、部署检查同样重要。再好的方案如果运维不到位照样出问题。后来我在所有 ID 生成服务里都加了启动自检检查时钟偏差、检查机器 ID 冲突、检查号段余量。自检不通过就拒绝启动从源头上避免问题。这个习惯帮我挡掉了好几次潜在故障。关于全局唯一 ID 这个话题能聊的还有很多比如 Leaf 的源码分析、百度的 UidGenerator、各种方案的性能对比测试。但核心思路就是这些理解需求、选对方案、做好容灾、完善监控。把这四点做到位ID 生成就不会成为系统的短板。