文章

分布式 ID

分布式 ID 系统设计指南

一、为什么需要分布式 ID?

1.1 数据库自增 ID 的局限性

核心痛点:

  • 单点故障风险:数据库宕机 → 整个系统瘫痪
  • 性能瓶颈:所有请求打到单库,TPS/QPS 有物理上限
  • 分库分表冲突:多库环境下自增 ID 会重复
  • 安全隐患:连续数字暴露业务量(竞争对手可推算真实销售额)

常见错误理解:

“每次生成 ID 都必须向数据库发起 SQL” - 号段模式已优化此问题

1.2 分布式 ID 的核心指标

评估方案时的五个维度:

  1. 唯一性:跨节点、跨时间绝对不重复
  2. 有序性:是否支持单调递增(影响数据库索引性能)
  3. 可用性:故障时的影响范围
  4. 性能:QPS 上限与延迟(微秒级 vs 毫秒级)
  5. 扩展性:适应业务变更的能力

二、主流方案对比

2.1 UUID

  • 优点:本地生成、无网络开销
  • 缺点:无序导致数据库索引性能差、字符串占用空间大
  • 适用场景:临时令牌、非主键标识

2.2 数据库自增/Redis 自增

  • 优点:实现简单、ID 有序
  • 缺点:中心化依赖、高并发下性能受限

详解: | 方案 | 瓶颈原因 | QPS 上限 | |——|———-|———| | DB 自增 | 磁盘 I/O + 锁竞争 | ~5000 | | Redis 自增 | 网络 I/O + 单线程 | ~10 万 |

2.3 雪花算法(Snowflake)

  • 结构:1 位符号 + 41 位时间戳 + 10 位机器 ID+12 位序列号
  • 优点:本地生成、高性能、趋势递增
  • 缺点:强依赖时钟,存在时钟回拨风险
  • QPS:单机百万级

2.4 美团 Leaf / 百度 UidGenerator

工业级解决方案,解决了原生雪花的痛点

特性 Leaf-Snowflake Leaf-Segment 百度 UidGenerator
依赖 ZooKeeper MySQL RingBuffer 内存
性能 5 万 + QPS 5 万 + QPS 600 万 + QPS
容灾 ZK 持久化 双 Buffer 机制 预发号缓存
运维成本
适用场景 容器化环境 核心交易 海量日志

2.5 三种 ID 对数据库(B+ 树)的影响

前提:主键索引底层是 B+ 树,页满就要分裂、树变深——ID 的有序性直接决定写库时是”排队”还是”插队”。

对比项 自增 ID 雪花 ID UUID
插入位置 永远末尾追加 末尾追加(趋势递增) 随机落任意叶子页中间
页分裂 极低,只尾页分裂 低,只尾页分裂 高,到处分裂,可能连锁到父层、树加高
分裂后空间 旧页是满的,不浪费 同左 两页都半满,浪费
索引体积/性能 最小、最快 小、快 大(存字符串),又厚又松,写库随机 I/O

一句话记忆:自增/雪花 = 排队(有序追加,几乎不分裂);UUID = 插队(随机乱插,整棵树到处都是半空的页)。

结论

  1. 能用自增就用自增;要全局唯一就用雪花这类趋势递增 ID,效果最接近自增
  2. 删除/更新多也会留半空页(碎片)→ 定期 OPTIMIZE TABLE 压实

三、核心技术原理

3.1 雪花算法的时钟回拨问题

什么是时钟回拨?

当 NTP 时间同步服务器将本地时钟往回调整时,雪花算法会生成重复 ID。

三种解决策略

  1. 轻度回拨(≤5ms):原地等待(Sleep),等时间追平
  2. 中度回拨:动态切换 WorkerID(需放大 WorkerID 位数至 22 位)
  3. 重度回拨(>5ms):直接抛异常,触发告警

为什么 Leaf 官方选择”宁死不报假号”?

  • “绝对不重复”是底线,优先级高于”高可用”
  • 自动换 WorkerID 需要清空内存缓存、协调 ZK、持久化状态,实现复杂且容易出错
  • 时钟回拨是服务器异常,应该告警修复根因,而不是自动绕过掩盖问题

业界加固方案(二次开发)

| 方案 | 原理 | 代价 | |——|——|——| | 机器 ID 换绑 | 向 ZK 申请新 WorkerID | 压缩时间戳/序列号位数 | | 序列号扩展 | 增加”回拨次数标记位” | 降低单毫秒并发上限 | | 时间持久化 | 启动前校验,偏差大则拒绝启动 | 需配合负载均衡摘除节点 |

3.2 号段模式(Segment)的双 Buffer 机制

痛点

没有双 Buffer 时:号段用尽 → 同步等数据库 → 阻塞所有业务请求

运作流程

graph LR
    A["Buffer A<br/>当前抽屉"] -->|"业务发号使用"| B["消耗到 10% 阈值"]
    B -->|"触发"| C["后台异步<br/>从数据库进货"]
    C --> D["提前装满<br/>Buffer B"]
    D --> E["瞬间切换到 B<br/>角色互换"]
    E --> A

核心价值

  • 消灭毛刺:数据库 IO 被完全异步化
  • 掩盖延迟:业务方永远感知不到”进货”动作
  • 容灾能力:即使 DB 宕机,内存号段仍能支撑一段时间

3.3 WorkerID 分配机制

Starter 模式(内嵌)

  • 每个业务实例各自连 ZK 领 WorkerID
  • 首次获取后缓存在本地文件,ZK 宕机不影响已运行服务
  • 缺点:N 个服务 × M 个实例 = ZK 连接数多

独立部署模式

  • 只有 Leaf 服务连 ZK,业务服务不感知
  • ID 生成中心化为独立服务,统一管控
  • 缺点:每次调用产生网络消耗(RPC/HTTP)
  • 优点:ZK 连接数少,只管理 Leaf 集群

四、选型决策指南

4.1 场景匹配矩阵

业务类型 推荐方案 理由
电商订单 Leaf-Segment 数据库索引友好、性能足够
支付流水 Leaf-Segment 强一致性、趋势递增
日志追踪 Leaf-Snowflake 超高并发、不依赖数据库
消息队列 Leaf-Snowflake 无网络 IO、纯内存计算
K8s 容器环境 Leaf-Snowflake WorkerID 自动分配
对账审计 Leaf-Segment 严格递增可追溯

4.2 架构权衡要点

独立部署 vs Starter 模式

| 维度 | 独立部署 | Starter 内嵌 | |——|———-|————-| | 网络消耗 | 有(1~5ms) | (纳秒级) | | 运维成本 | 高 | | | 性能瓶颈 | 网络带宽 | CPU 计算 | | 架构解耦 | 完全解耦 | 业务耦合 | | 最佳规模 | >10 个服务 | <5 个服务 |

何时必须用独立部署?

  1. 公司级统一 ID 管控需求
  2. 几十个微服务需要集中管理
  3. 需要独立监控/调优 ID 生成参数

4.3 安全加固方案

防业务量泄露

问题:连续自增 ID → 竞争对手可通过差值推算订单量

解决方案:

  1. 内外 ID 隔离(大厂标配)
    1
    2
    3
    4
    5
    6
    
    // 内部存 Leaf ID(自增)
    long internalId = leafService.getSegmentId();
       
    // 对外返回加密字符串(Hashids)
    String externalId = hashIds.encode(internalId);
    // 输出:xY7bQ (无法推测)
    
  2. 超大起始值 + 随机步长
    1
    2
    3
    
    -- 初始 max_id 设为天文数字
    UPDATE leaf_alloc SET max_id = 100000000;
    -- 步长可调为动态随机值
    
  3. 接口层防越权(必须实施)
    1
    2
    3
    4
    5
    6
    
    // ❌ 错误:仅凭 ID 查询
    SELECT * FROM orders WHERE id = 10001;
       
    // ✅ 正确:用户 ID 联合校验
    SELECT * FROM orders 
    WHERE id = 10001 AND user_id = currentUser.id;
    

五、常见误区澄清

5.1 NTP 与时间回拨的关系

误区:”NTP 可以解决时钟回拨”
真相:NTP 是导致回拨的诱因之一,而非解决方案

1
2
正确理解链:
硬件漂移 → NTP 校准 → 时间回调 → Leaf 检测回拨 → 执行防御策略

5.2 号段模式的连续性

误区:”号段模式 ID 绝对连续”
真相:服务重启会导致 ID 空洞(未使用的号段丢失)

1
2
3
4
5
举例说明:
正常运行:[1001, 3000] → 用到 1050
服务挂掉:内存清空,损失 1950 个 ID
重启后:基于 DB 的 max_id=3000 重新申请 [3001, 5001]
结果:ID 从 1050 跳到 3001,中间断号

影响评估:

  • ✅ 订单/交易:完全可以接受
  • ⚠️ 财务流水:需额外标注序号偏移
  • ❌ 发票/排队号:不能用分布式 ID

5.3 分布式 ID vs 分布式锁

完全不同

  • 分布式 ID:全局唯一标识(如订单号)
  • 分布式锁:并发控制(如秒杀扣库存)

关联点:分布式 ID 可作为锁的唯一标识,但不能替代锁机制


六、面试应答模板

Q:为什么不用数据库自增 ID?

回答结构

  1. 风险:单点故障、性能瓶颈、分库冲突
  2. 对比:展示 Leaf/雪花在 QPS 上的量化优势
  3. 案例:美团日均亿级调用,Leaf 处理 99.999% 可用性

Q:雪花算法如何解决时钟回拨?

回答要点

  • 轻度回拨:5ms 内等待
  • 重度回拨:抛异常 + 告警
  • 业界增强:WorkerID 换绑或序列号扩展

Q:号段模式和雪花模式怎么选?

回答框架

1
2
3
4
5
6
7
8
9
10
11
1. 先判断业务特征:
   - 需要严格递增?→ 选 Segment
   - 极致性能优先?→ 选 Snowflake

2. 再考虑技术栈:
   - 不想运维 ZK?→ 选 Segment
   - 已经在用 K8s?→ 选 Snowflake

3. 最后混合策略:
   - 核心业务(订单):Segment
   - 辅助业务(日志):Snowflake

附录:参考资源

  • 美团 Leaf 官方:https://github.com/Meituan-Dianping/Leaf
  • 技术文章:https://tech.meituan.com/2019/03/07/open-source-project-leaf.html
  • 百度 UidGenerator:https://github.com/baidu/uid-generator
本文由作者按照 CC BY 4.0 进行授权