分布式唯一 ID:为什么 UUID 不够用?
分库分表之后,单库自增主键不再能保证全局唯一;而 UUID 虽然生成简单,却存在空间占用大、写入索引不友好等问题。真正适合亿级流量系统的唯一 ID,需要在全局唯一、空间占用、高并发、高可用和数据库主键友好之间取得平衡。
一、核心主线
在分库分表和分布式系统中,如何生成全局唯一、性能高、可用性强且适合作为数据库主键的 ID。
整体演进逻辑:
1 | 单体数据库自增主键 |
本节重点明确唯一 ID 的设计目标,并区分了单调递增和趋势递增两种类型。
二、为什么需要分布式唯一 ID
1. 单体数据库中的自增主键
在单体架构中,一个业务实体通常对应一张数据表。
1 | 用户表 → 数据库自增主键 |
由于所有数据都在同一个数据库中,数据库自增主键可以保证表内唯一。
2. 分库分表后的问题
随着用户量增长,数据库会进行分库分表。
1 | 用户数据 |
每个数据库拥有独立的自增序列:
1 | 数据库 1:1、2、3... |
不同数据库可能生成相同的 ID。
如果仍将数据库自增主键作为业务实体的唯一 ID,就会出现:
- 不同数据拥有相同标识;
- 跨库查询无法准确定位数据;
- 数据关联关系错误;
- 业务产生严重事故。
因此,需要一种跨数据库、跨节点仍然唯一的 ID:
分布式唯一 ID。
三、UUID
1. 基本概念
RFC 4122 定义了 UUID(Universally Unique Identifier,通用唯一识别码)。
UUID 是一个 128 位标识符,按照标准方法生成时,重复概率极低。
标准字符串格式:
1 | 8-4-4-4-12 |
例如:
1 | 6a0d3e6f-a11c-4b7d-bb35-c4c530a456b0 |
2. UUID 的优势
- 不依赖中心服务;
- 本地即可生成;
- 不需要访问数据库;
- 生成逻辑简单;
- 全局重复概率极低。
3. UUID 的局限
3.1 存储空间较大
UUID 字符串通常占用 36 个字符。
如果海量数据的每条记录都保存 UUID:
1 | 数据量巨大 × 每条 36 字节 |
会产生较大的存储空间消耗。
3.2 不适合作为数据库主键
UUID 是无规律的长字符串,用作数据库主键时,新数据的 Key 可能随机插入索引中间位置。
这会导致:
- 数据页频繁移动;
- B+ 树分页;
- 磁盘碎片增加;
- 写入性能下降。
因此,本节认为 UUID 更适合:
- 数据量不大的场景;
- 标识存储分区;
- 不直接作为海量业务表主键的场景。
四、海量场景下唯一 ID 的设计要求
真正适合大型互联网系统的唯一 ID 生成器,除了保证不重复,还应具备以下特点。
1. 空间占用小
唯一 ID 通常会被每条业务数据携带,因此不应占用过多存储空间。
书中倾向于使用:
1 | 64 位 long 类型整数 |
它只占用 8 字节,同时可表示很大的数值范围。
2. 高并发
唯一 ID 生成器是大量业务服务的基础依赖。
它必须能够:
- 承受高并发生成请求;
- 低延迟返回 ID;
- 避免成为业务链路瓶颈。
3. 高可用
如果唯一 ID 生成器不可用,依赖它的用户、内容、订单等业务都可能无法创建新数据。
因此需要:
- 持续稳定提供服务;
- 避免单点故障;
- 支持故障恢复;
- 保证 ID 分配不重复。
4. 适合作为数据库主键
唯一 ID 不应严重影响数据库写入性能。
理想情况下,ID 应具有递增特征,使数据库索引能够顺序写入。
五、InnoDB 主键与 B+ 树
1. InnoDB 的索引结构
MySQL InnoDB 使用磁盘上的 B+ 树组织数据,并以主键作为索引。
B+ 树的特点:
- 节点使用数据页保存;
- 默认数据页大小约为 16 KB;
- 节点分为叶节点和非叶节点;
- 非叶节点保存主键和子页索引;
- 叶节点保存实际数据;
- 同层叶节点通过双向链表连接;
- 数据按照主键从小到大排列。
2. 递增主键的好处
使用自增或递增 ID 作为主键:
1 | 新数据 |
优点:
- 数据顺序写入;
- 索引结构更紧凑;
- 空间利用率更高;
- 不需要频繁移动旧数据;
- 分页操作较少;
- 写入性能较好。
3. 随机主键的问题
如果使用身份证号、电话号码或 UUID 等随机值作为主键:
1 | 新 Key 可能插入 B+ 树中间 |
可能导致:
- 已有记录向后移动;
- 数据页频繁分页;
- 磁盘产生更多碎片;
- 写入耗时增加;
- 索引空间利用率下降。
因此,数据库主键最好使用递增性质的 ID。
六、单调递增与趋势递增
1. 单调递增
单调递增表示:
后一次生成的 ID 一定大于前一次生成的 ID。
形式化表示:
1 | T(n+1) > T(n) |
特点:
- 全局严格有序;
- 任何下一次生成的 ID 都比上一次大;
- 最适合严格顺序需求;
- 分布式实现复杂;
- 容易受到全局时钟、网络延迟和并发调度影响。
2. 趋势递增
趋势递增表示:
后生成的 ID 大概率比先生成的 ID 大,但短时间内可能出现乱序。
形式化理解:
1 | T(n+1) > T(n) |
特点:
- 整体趋势递增;
- 局部允许少量乱序;
- 更适合分布式系统;
- 实现成本和性能通常更好;
- 对数据库主键仍然比较友好。
3. 为什么业界更偏好趋势递增
在分布式环境中:
- 各节点时钟可能存在差异;
- 网络延迟不同;
- 请求到达顺序和完成顺序可能不同;
- 多节点并发生成会导致严格全局排序困难。
因此,严格单调递增通常需要更复杂的协调机制。
趋势递增允许少量局部乱序,但能更好地平衡:
- 分布式生成;
- 高并发;
- 高可用;
- 数据库写入性能。
七、唯一 ID 类型对比
| 类型 | 唯一性 | 递增性 | 空间占用 | 生成方式 | 适用情况 |
|---|---|---|---|---|---|
| 数据库自增主键 | 单库内唯一 | 单调递增 | 小 | 依赖数据库 | 单体或单库场景 |
| UUID | 全局唯一概率极高 | 无序 | 较大 | 本地生成 | 低数据量、非主键标识 |
| 单调递增 ID | 全局唯一且严格递增 | 严格递增 | 可较小 | 需要中心协调 | 强顺序需求 |
| 趋势递增 ID | 全局唯一 | 整体递增、局部可能乱序 | 可较小 | 分布式生成 | 海量数据和高并发场景 |
八、唯一 ID 生成器的设计方向
1 | 生成 64 位 long 类型 |
九、核心架构思想
- 分库分表后,单库自增主键无法保证全局唯一。
- 分布式唯一 ID 必须跨数据库、跨节点保持不重复。
- UUID 本地生成简单,但空间占用大且不适合作为海量数据库主键。
- 唯一 ID 生成器不仅要唯一,还要满足低空间、高并发、高可用和主键友好。
- InnoDB B+ 树更适合使用递增主键进行顺序写入。
- 随机主键会增加数据移动、分页、碎片和磁盘 I/O。
- 64 位 long 类型在空间占用和可表示范围之间取得了较好平衡。
- 单调递增要求全局严格有序,分布式实现复杂。
- 趋势递增允许局部乱序,但整体递增,更适合互联网分布式系统。
- 唯一 ID 生成方案的核心权衡是唯一性、顺序性、性能、可用性和实现复杂度。
十、一句话总结
分库分表后必须使用跨节点全局唯一的分布式 ID;UUID 虽然生成简单但空间大且不利于数据库写入,适合海量业务数据的 ID 应尽量使用 64 位整数,并具备高并发、高可用和递增特征,其中严格单调递增实现复杂,而允许局部乱序的趋势递增更适合分布式系统和 InnoDB 主键。