输入关键词,开始搜索

↑↓ 选择 打开 esc 关闭
沉浸

系统设计:分布式唯一 ID 生成器

13 min read
  1. 1 系统设计:分布式唯一 ID 生成器
  2. 2 系统设计:配置中心

专栏定位

这个系列为已经会用 AI 写代码的读者准备——重点是学会设计系统。每篇文章都是完整的系统设计过程,从需求分析到架构设计到关键技术决策,同时标注面试追问点。

阅读建议:初级读者重点看架构大图和核心模块;中级读者深入扩展点和方案对比。

背景与问题

为什么需要全局唯一 ID

分布式系统中,数据分散在多台机器、多个数据库实例上。单机自增 ID 在分库分表后会产生冲突:

DB-1: 1, 2, 3, 4, 5...
DB-2: 1, 2, 3, 4, 5...  ← 冲突!

典型需要全局唯一 ID 的场景:

  • 订单号、支付流水号(业务唯一标识)
  • 分布式数据库主键(分库分表后)
  • 消息队列 Message ID(去重、追踪)
  • 日志 Trace ID(全链路追踪)

单机自增 ID 的问题

问题说明
冲突多实例同时自增,ID 重复
单点瓶颈所有写请求依赖单台数据库
数据迁移困难自增 ID 与业务数据耦合,分片后难以迁移
信息泄露连续 ID 暴露业务量(如订单号)

需求分析

功能需求

需求描述优先级
全局唯一任意时间、任意节点生成的 ID 不重复P0
趋势递增ID 大致按时间递增,便于 B+ 树索引P0
低延迟生成 ID 的耗时在毫秒级P0
高可用ID 生成服务不能成为单点故障P0
可反解能从 ID 中解析出时间、机器等信息(可选)P1

非功能需求

指标目标值
生成延迟P99 < 1ms
可用性99.99%
单机 QPS> 10万
ID 长度64 位整数(主流)或 128 位字符串

核心指标估算

假设服务一个日活千万级的电商平台:

QPS 估算

场景计算峰值 QPS
订单创建1000万订单/天 ÷ 86400s × 10(峰值系数)~1,200
支付流水与订单 1
~1,200
消息队列内部消息,约为订单 5 倍~6,000
日志追踪每个请求产生多条日志~50,000
合计~6万 QPS

ID 存储量

项目计算结果
日生成 ID 数订单 + 支付 + 消息 + 日志~10 亿/天
年生成 ID 数10亿 × 365~3650 亿
64 位 ID 存储/年3650亿 × 8 字节~292 GB

10 亿/天 ≈ 12万 QPS 平均,峰值可达 50万 QPS。ID 生成器必须支撑这个量级。

总体架构

graph TB
    subgraph "业务应用层"
        App1["订单服务"]
        App2["支付服务"]
        App3["消息服务"]
        App4["日志服务"]
    end

    subgraph "ID 生成层"
        IDGen["ID 生成器集群<br/>多实例无状态"]
    end

    subgraph "ID 分配策略"
        A["方案A<br/>数据库自增"]
        B["方案B<br/>UUID"]
        C["方案C<br/>Snowflake"]
        D["方案D<br/>号段模式"]
        E["方案E<br/>号段+缓存"]
    end

    App1 --> IDGen
    App2 --> IDGen
    App3 --> IDGen
    App4 --> IDGen

    IDGen -.-> A
    IDGen -.-> B
    IDGen -.-> C
    IDGen -.-> D
    IDGen -.-> E

    style App1 fill:#e1f5fe
    style App2 fill:#e1f5fe
    style App3 fill:#e1f5fe
    style App4 fill:#e1f5fe
    style IDGen fill:#fff3e0
    style A fill:#fce4ec
    style B fill:#fce4ec
    style C fill:#e8f5e9
    style D fill:#e8f5e9
    style E fill:#e8f5e9

方案详解

方案 A:数据库自增 ID

原理:利用 MySQL 的 AUTO_INCREMENT 生成唯一 ID。

sequenceDiagram
    actor App as 业务应用
    participant DB as MySQL

    App->>DB: INSERT INTO id_table (stub) VALUES ('a')
    DB-->>App: 返回自增 ID: 10001

实现代码

CREATE TABLE id_table (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    stub CHAR(1) NOT NULL DEFAULT ''
);
-- 每次插入获取新 ID
INSERT INTO id_table (stub) VALUES ('a');
SELECT LAST_INSERT_ID();
优点缺点
实现简单,无需额外服务单点瓶颈,QPS 上限约几千
严格递增,连续无空洞数据库挂了无法生成 ID
天然唯一,无需冲突处理不适合分库分表场景

适用场景:低并发、单体应用、原型验证。


方案 B:UUID / GUID

原理:基于时间戳、机器 MAC 地址、随机数生成 128 位唯一字符串。

示例:550e8400-e29b-41d4-a716-446655440000
      └─时间─┘ └─版本┘ └─变体┘ └────随机数────┘

UUID 版本对比

版本生成依据特点
V1时间戳 + MAC 地址有时间顺序,暴露 MAC 地址
V4完全随机最常用,无时间顺序
V7时间排序 + 随机2022 年新 RFC,支持排序
优点缺点
本地生成,无网络依赖128 位太长,存储和索引成本高
理论上不会冲突无序,数据库插入性能差(随机写)
高可用,无需中心服务不可读,无法从 ID 获取信息
生成速度极快不适合做主键(页分裂严重)

适用场景:日志 ID、临时令牌、对长度不敏感的场景。

关键认知:UUID V4 完全随机,插入 MySQL InnoDB 时会导致频繁的页分裂,性能比自增 ID 差 5-10 倍。UUID V7 解决了排序问题,是更好的选择。


方案 C:Snowflake 算法

原理:Twitter 开源的 64 位 ID 生成算法,将 ID 划分为多个部分。

graph LR
    subgraph "64 位 Snowflake ID 结构"
        direction LR
        S1["符号位<br/>1 bit<br/>固定为 0"]
        S2["时间戳<br/>41 bit<br/>毫秒级"]
        S3["机器 ID<br/>10 bit<br/>最多 1024 节点"]
        S4["序列号<br/>12 bit<br/>每毫秒 4096 个"]
    end

    style S1 fill:#ffebee
    style S2 fill:#e8f5e9
    style S3 fill:#e3f2fd
    style S4 fill:#fff3e0

位分配详解

字段位数说明范围
符号位1 bit固定为 0,保证正数0
时间戳41 bit自定义起始时间的毫秒偏移~69 年
机器 ID10 bit数据中心 + 机器编号0-1023
序列号12 bit同一毫秒内自增0-4095

计算能力

  • 每毫秒每节点可生成 4096 个 ID
  • 单节点理论 QPS:4096 × 1000 = 409.6 万/秒
  • 1024 节点集群理论 QPS:42 亿/秒
sequenceDiagram
    actor App as 业务应用
    participant SF as Snowflake 实例

    App->>SF: 请求生成 ID
    SF->>SF: 获取当前时间戳(毫秒)
    SF->>SF: 与上一时间戳比较

    alt 同一毫秒
        SF->>SF: 序列号 +1
    else 新毫秒
        SF->>SF: 序列号归零
    end

    SF->>SF: 拼接:时间戳 | 机器ID | 序列号
    SF-->>App: 返回 64 位 ID

优点

  • 本地生成,无网络开销(延迟 < 1ms)
  • 趋势递增,适合数据库索引
  • 可反解:从 ID 中提取时间和机器信息
  • 高吞吐,单机百万级 QPS

缺点

  • 依赖机器时钟,时钟回拨会导致重复 ID
  • 需要预先分配机器 ID,运维复杂
  • 41 位时间戳约 69 年,存在溢出风险

适用场景:高并发分布式系统,是目前业界最主流的方案。


方案 D:号段模式(Leaf-segment)

原理:美团 Leaf 开源方案。从数据库批量申请一个 ID 号段,在内存中顺序分配,用完再申请。

sequenceDiagram
    actor App as 业务应用
    participant Leaf as Leaf 服务
    participant DB as MySQL

    Note over App,DB: 初始化:申请号段 [1, 1000]

    App->>Leaf: 请求 ID
    Leaf->>Leaf: 内存分配:返回 1
    Leaf-->>App: ID = 1

    App->>Leaf: 请求 ID
    Leaf->>Leaf: 内存分配:返回 2
    Leaf-->>App: ID = 2

    Note over Leaf: ... 分配到 1000,号段用完

    Leaf->>DB: UPDATE leaf_alloc SET max_id = 2000
    DB-->>Leaf: 号段更新为 [1001, 2000]

    App->>Leaf: 请求 ID
    Leaf->>Leaf: 内存分配:返回 1001
    Leaf-->>App: ID = 1001

数据库表结构

CREATE TABLE leaf_alloc (
    biz_tag VARCHAR(128) NOT NULL PRIMARY KEY,  -- 业务标识
    max_id BIGINT NOT NULL DEFAULT 1,            -- 当前最大号段
    step INT NOT NULL DEFAULT 1000,              -- 每次申请步长
    description VARCHAR(256)
);

双 Buffer 优化

  • 号段使用到 10% 时,异步申请下一个号段
  • 避免号段用完时的同步等待
优点缺点
趋势递增,严格有序号段用完时申请有延迟尖刺
数据库压力小(批量申请)需要额外 Leaf 服务部署
ID 是连续整数,存储高效突发流量下可能号段消耗过快
可自定义起始值和步长服务重启可能丢失未用完号段

适用场景:需要严格递增、对数据库友好、可接受轻微延迟尖刺的业务。


方案 E:号段 + 缓存模式(Leaf-snowflake 改进)

原理:结合号段模式和 Snowflake 的优点,用 ZooKeeper/Etcd 分配机器 ID,本地 Snowflake 生成 ID。

graph TB
    subgraph "注册中心"
        ZK["ZooKeeper / Etcd"]
    end

    subgraph "Leaf 服务集群"
        L1["Leaf 实例-1<br/>机器ID: 1"]
        L2["Leaf 实例-2<br/>机器ID: 2"]
        L3["Leaf 实例-N<br/>机器ID: N"]
    end

    subgraph "业务应用"
        App1["订单服务"]
        App2["支付服务"]
    end

    ZK -->|"分配机器ID"| L1
    ZK -->|"分配机器ID"| L2
    ZK -->|"分配机器ID"| L3

    L1 -->|"提供 ID 生成 API"| App1
    L2 -->|"提供 ID 生成 API"| App2
    L3 -->|"提供 ID 生成 API"| App1

    style ZK fill:#fff3e0
    style L1 fill:#e8f5e9
    style L2 fill:#e8f5e9
    style L3 fill:#e8f5e9
    style App1 fill:#e1f5fe
    style App2 fill:#e1f5fe

工作流程

  1. Leaf 实例启动时向 ZooKeeper 注册,获取唯一机器 ID
  2. 本地使用 Snowflake 算法生成 ID
  3. ZooKeeper 监控实例心跳,实例下线回收机器 ID
  4. 时钟回拨时,等待或从 ZooKeeper 重新校准
优点缺点
无单点,可水平扩展依赖 ZooKeeper/Etcd
解决 Snowflake 机器 ID 分配问题架构复杂度较高
可检测时钟回拨并处理需要维护注册中心
兼具号段模式的有序性和 Snowflake 的高性能网络分区时可能有问题

适用场景:大规模分布式系统,需要高可用、高吞吐、自动扩缩容。

方案对比总结

维度数据库自增UUID V4Snowflake号段模式号段+Snowflake
唯一性理论唯一
有序性严格递增完全无序趋势递增严格递增趋势递增
长度64 bit128 bit64 bit64 bit64 bit
单机 QPS~1,000>100万>400万>10万>400万
生成延迟~10ms~1μs~1μs~1ms~1μs
网络依赖强依赖 DB依赖 DB(批量)依赖 ZK(启动时)
时钟依赖强依赖强依赖
可反解信息时间+机器时间+机器
运维复杂度极低
推荐度⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐

选型建议

  • 单体应用/低并发:数据库自增
  • 日志/令牌/不敏感场景:UUID V7
  • 高并发分布式系统(默认推荐):Snowflake
  • 需要严格递增 + 批量优化:号段模式
  • 超大规模 + 自动扩缩容:号段 + Snowflake

面试追问点

Q1:Snowflake 的时钟回拨问题怎么解决?

问题:NTP 同步或手动调整时间导致机器时钟回退,同一毫秒内可能生成重复 ID。

解决方案

  1. 等待策略:发现回拨时,暂停生成直到时钟追上上次记录的时间戳。适合回拨幅度小(< 5ms)的情况。

  2. 备用位策略:预留几位作为「回拨序列号」,时钟回拨时启用备用位。牺牲少量容量换取容错。

  3. 历史时间戳缓存:记录最近 N 毫秒的时间戳序列,回拨时从历史序列中分配。实现复杂,极少使用。

  4. Leaf 方案:依赖 ZooKeeper 检测,回拨超过阈值时报警并拒绝服务,等待人工介入或自动重启。

最佳实践:业务上允许 5ms 内的回拨等待,超过则抛异常。同时监控时钟回拨报警。

Q2:号段模式在取号段时如果 DB 挂了怎么办?

问题:号段用完需要向数据库申请新号段,此时数据库不可用会导致 ID 生成中断。

解决方案

  1. 双 Buffer:当前号段用到 10% 时就异步加载下一个号段。正常情况下新号段早已准备好,DB 短暂故障不影响。

  2. 号段缓存持久化:将当前号段信息写入本地文件,服务重启时恢复,避免号段浪费。

  3. 降级方案:DB 长时间不可用时,切换到备用 ID 生成策略(如本地 Snowflake 临时模式),保证核心业务不中断。

  4. 数据库高可用:MySQL 主从 + Sentinel,或者使用 TiDB 等分布式数据库。

Q3:UUID 为什么不适合做数据库主键?

核心原因:InnoDB 使用 B+ 树索引,数据按主键顺序存储。

自增 ID 插入UUID 插入
顺序追加到页尾随机插入到各页
几乎无页分裂频繁页分裂
磁盘顺序写,效率高磁盘随机写,效率低
缓存命中率高缓存频繁失效

量化影响

  • UUID 做主键,插入性能比自增 ID 差 5-10 倍
  • 数据页利用率从 ~90% 降到 ~50%
  • 索引文件大小增加约 2 倍

替代方案:使用 Snowflake(趋势递增)或 UUID V7(时间排序)。

Q4:如何设计一个支持 64 位和 128 位两种 ID 格式的系统?

设计思路

  1. 统一接口层:对外暴露 generateId(format) 接口,format 参数指定 INT64INT128

  2. 内部路由

    • INT64 -> Snowflake 生成器
    • INT128 -> 扩展版 Snowflake 或 UUID V7
  3. 128 位扩展方案

| 1 bit | 48 bit 时间戳 | 16 bit 数据中心 | 32 bit 机器ID | 31 bit 序列号 |
  • 时间戳精度提升到微秒
  • 支持更多机器节点(2^32 = 42 亿)
  • 序列号空间更大,应对更高并发
  1. 兼容性处理
    • 数据库用 VARCHAR(36) 存储,兼容两种格式
    • 或分两个字段 id_int64id_int128,根据业务选择

Q5:如果 ID 要支持排序且能反解出时间信息,怎么设计?

需求拆解

  • 排序:ID 按时间趋势递增
  • 反解:从 ID 中提取生成时间

方案

  1. Snowflake 天然满足

    • 高位是时间戳,天然有序
    • 反解公式:timestamp = (id >> 22) + custom_epoch
  2. 自定义编码(更灵活)

| 32 bit 秒级时间戳 | 16 bit 业务编码 | 16 bit 机器ID |
  • 32 位时间戳支持到 2106 年
  • 业务编码区分订单、支付、消息等
  • 反解时直接取高 32 位即为时间戳
  1. 字符串格式(可读性优先)
202604251530009876543210
└─┬─┘└┬┘└┬┘└┬─┘└──┬───┘
  年月日时分秒   毫秒    序列号
  • 优点:人类可读,直接看出时间
  • 缺点:字符串存储和比较开销大

扩展思考

如果 10 万 QPS 的 ID 生成需求,选哪个方案?

方案10万 QPS 表现结论
数据库自增单库上限 ~1000 QPS,需要 100+ 个数据库实例❌ 不可行
UUID V4本地生成,10万 QPS 无压力,但无序⚠️ 可用但不推荐
Snowflake单实例 400万 QPS,1 个实例搞定首选
号段模式单实例 10万+ QPS,但号段申请有延迟尖刺✅ 可用
号段+Snowflake完全无压力,但运维成本高✅ 大规模推荐

推荐

  • 10万 QPS:Snowflake 单机部署,简单高效
  • 100万 QPS:Snowflake 集群(3-5 实例)+ 负载均衡
  • 1000万 QPS:号段 + Snowflake + 自动扩缩容

实际生产中的注意事项

  1. 机器 ID 分配:小规模用配置文件,大规模用 ZooKeeper/Etcd 自动分配
  2. 时间戳起始点:自定义 epoch(如 2024-01-01),延长可用时间
  3. 监控报警:时钟回拨、QPS 突增、ID 耗尽(接近最大值)
  4. 单元化部署:多地域部署时,机器 ID 中预留地域位

专栏导航

本文是「系统设计专栏」第 2 篇:分布式唯一 ID 生成器

上一篇:短链服务

下一篇:一致性哈希

© 本文著作权归作者所有,转载请注明出处。

Miki
Miki
未知标题
00:00
00:00
播放列表