Skip to content

让你设计一个分布式 ID 发号器,怎么设计? ​

  • 标签:后端、系统设计、场景题

🏛 业务架构师 ​

先明确需求,分布式 ID 的业务硬指标:全局唯一、趋势递增(对 B+ 树索引友好)、高可用、高并发。安全性加分项:不可被枚举(订单号不泄露单量)。

方案对比按业务阶段选:

  • UUID:本地生成零依赖,但无序(索引页分裂、缓存命中率灾难)且太长(36 字符)——不适合做主键,只适合做无索引诉求的追踪 ID;
  • 数据库号段模式(Leaf-segment 思路):DB 存当前最大号,一次取一段(如 1 万个)缓存在应用内存慢慢发——DB 压力降 4 个数量级,双 buffer 预取平滑切换。简单可靠,我的默认推荐;缺点:DB 挂了发号受影响(缓存号段可扛一阵)、ID 连续可枚举;
  • 雪花算法(Snowflake):41 位时间戳 + 10 位机器 + 12 位序列——无单点、毫秒级 400 万+ 容量、趋势递增。难点是workerId 分配与时钟回拨:机器时间被 NTP 往回调会发重号——要用 NTP 监控 + 回拨拒绝/等待策略,或用美团 Leaf-snowflake(ZK 分配 workerId + 时钟校验);
  • Redis incr:性能好,但Redis 是新的单点与一致性依赖,ID 这种基础设施级服务押在缓存组件上不划算。

业务设计要点(比算法更重要):订单号承载业务语义——时间(客服一眼判断单据时间)、分库路由位(按 hash 散库)、校验位(防输错)、机器位(排障定位)——ID 是数据资产的第一块砖,格式定死了很难改。

我的落地结论:中小规模数据库号段(双 buffer),大规模/多机房 Leaf-snowflake,别自造新算法。

🏗 系统架构师 ​

分布式 ID 的需求:全局唯一、趋势递增(有利 InnoDB 索引)、高可用、高吞吐(10 万+/s)、位数可控。我按方案对比再给设计:

主流方案对比:

  • UUID:本地生成无依赖,但 128 位无序——做主键导致页分裂、索引膨胀,只适合做 traceId/请求 ID;
  • DB 自增/号段模式:每次从 DB 取一个号段(如 1000 个)缓存本地分配,DB 压力骤减,双 buffer 预取无缝续段;美团的 Leaf-segment 就是这个方案——简单可靠,缺点是重启浪费号段、依旧依赖 DB;
  • 雪花算法 Snowflake:64 位 = 1 符号 + 41 时间戳(69 年)+ 10 机器 ID(1024 节点)+ 12 序列(单机 409.6 万/s),本地生成无网络开销、趋势递增。命门是时钟回拨(NTP 校时回退会重复)——处理:回拨小则等待,大则报错/切备用 workerId,加上 lastTimestamp 校验;
  • Redis INCR/INCRBY:性能好但引入网络依赖与持久化风险,ID 连续有业务信息泄露风险(单号被推算单量)。

我的设计选择:雪花变体做默认(Leaf-snowflake 通过 ZK/DB 分配 workerId 解决手动配置;时钟回拨拒绝服务兜底);号段模式做备选(对递增严格连续不敏感、DB 可用的场景)。再叠加业务前缀(时间编码 + 机器位)做订单号可读性,序列化格式与溢出边界(毫秒内序列超 4096 自旋等下一毫秒)都要在设计文档明确。

💻 全栈开发专家 ​

先定需求再选方案:趋势递增、全局唯一、高性能、高可用、位数可控。主流三条路线:

1. UUID:本地生成无依赖,但无序(B+ 树插入页分裂,DB 性能差)、36 位太长。只适合日志 trace 等非主键场景。

2. DB 号段模式(美团 Leaf-segment):一次从 DB 批量取一段号,本地分发。

sql
CREATE TABLE leaf (
  biz_tag VARCHAR(64) PRIMARY KEY,
  max_id BIGINT NOT NULL,
  step INT NOT NULL DEFAULT 1000
);
-- 取号段:原子前推
UPDATE leaf SET max_id = max_id + step WHERE biz_tag = 'order';
SELECT max_id FROM leaf WHERE biz_tag = 'order';  -- 拿到 [max_id-step+1, max_id]

DB 压力 = 每秒 QPS/step,极小。双 buffer 优化:当前号段用到 10% 就异步预取下一段,消除取号段瞬间的抖动。DB 挂了还能靠本地余量撑一阵。缺点:重启浪费号段、ID 连续可被竞品推测量(隐私敏感场景要加混淆)。

3. 雪花算法 Snowflake:1位符号 + 41位时间戳(毫秒) + 10位机器ID + 12位序列号,单机每毫秒 4096 个,趋势递增、无外部依赖。

核心坑:时钟回拨——机器 NTP 校时往回调,可能生成重复 ID。我的处理:回拨小于 100ms 等待;大于则用"历史最大时间戳+1"继续(或切备用机器位);机器位用 ZK/DB 分配防冲突,绝不手工配置 workerId(重复机器号是事故源头)。

我的选型:并发中等用号段(连续可控);高并发用雪花;两者都上监控(取号失败率、回拨次数)。