存储拆分后,如何解决唯一主键问题?
0. 引言
分库分表后,数据库自增主键失效(各库自增会重复)。分布式 ID 需要满足:全局唯一、趋势递增(利于索引)、高性能(高并发下发)、高可用。本文梳理五种主流方案:从最简单的 UUID 到工业界标配的雪花算法,再到美团 Leaf 号段模式,并给出选型建议。
1. 分布式 ID 的硬性要求
| 要求 | 说明 |
|---|---|
| 全局唯一 | 任何时刻、任何节点生成的 ID 不重复 |
| 趋势递增/单调递增 | 递增 ID 利于 B+ 树索引插入(避免页分裂)与分页排序 |
| 高性能 | 高并发下下发延迟低(本地生成 > 远程获取) |
| 高可用 | 生成服务故障不影响业务 |
| 安全 | 不泄露业务量(如订单量、用户量) |
2. 五种方案对比
| 方案 | 唯一性 | 递增 | 性能 | 可用性 | 缺点 |
|---|---|---|---|---|---|
| UUID | ✅ | ❌ 随机 | 最高(本地) | 最高 | 36 字符、无序(索引页分裂)、无业务含义 |
| 数据库自增/号段 | ✅ | ✅ | 中 | 中(DB 依赖) | DB 单点/性能瓶颈(号段缓解) |
| Redis INCR | ✅ | ✅ | 高 | 高(集群) | 依赖 Redis、RDB 持久化可能丢号 |
| 雪花算法 | ✅ | ✅ 趋势 | 最高(本地生成) | 最高 | 时钟回拨风险、依赖机器号 |
| 号段模式(Leaf) | ✅ | ✅ | 高 | 高 | 实现复杂、依赖 DB 双机房 |
3. 各方案详解
3.1 UUID:最简单但最不推荐
java
String id = UUID.randomUUID().toString(); // 8-4-4-4-12 十六进制- 优点:本地生成、零依赖;
- 缺点:无序——作为主键写入 InnoDB 聚簇索引时频繁页分裂;36 字符过大(索引空间膨胀);不携带时间/业务信息。
3.2 数据库自增与号段模式
sql
-- 基础版:单库自增(不可用于分库分表)
CREATE TABLE sequence (id BIGINT AUTO_INCREMENT PRIMARY KEY);
-- 号段模式:一次取一段,应用内存中分配
CREATE TABLE leaf_alloc (
biz_tag VARCHAR(64) PRIMARY KEY, -- 业务标识(order/ user)
max_id BIGINT NOT NULL, -- 当前已分配的最大 ID
step INT NOT NULL, -- 号段步长(如 1000)
version INT NOT NULL -- 乐观锁版本
);- 号段模式流程:应用启动时
UPDATE ... SET max_id = max_id + step WHERE version=?取走一段(如 10001~11000),内存中递增分配,用尽再取; - 美团 Leaf-segment:双 buffer 预加载(当前段用 10% 时预取下一段)、双机房 DB 双写,趋势递增、可用性高;
- 缺点:ID 下发有批量性(可被预测)、依赖 DB。
3.3 Redis INCR
bash
INCR order:id:gen # 原子递增,返回唯一 ID- 优点:简单、性能高;
- 缺点:持久化丢号(RDB 快照间隔期间宕机可能重复)、Redis 故障依赖集群;可用 Lua + 时间戳前缀 组合优化。
3.4 雪花算法(Snowflake):工业界标配
text
64 位 long:
┌──────────────┬────────────┬─────────────┬──────────────┐
│ 1 bit 符号位 │ 41 bit 时间戳 │ 10 bit 机器号 │ 12 bit 序列号 │
│ (固定 0) │ (毫秒级) │ (5bit 机房+5bit 机器)│ (同毫秒内自增) │
└──────────────┴────────────┴─────────────┴──────────────┘- 组成:
(当前毫秒 - 起始毫秒) << 22 | 机器号 << 12 | 毫秒内序列号; - 单机每秒可生成 409.6 万个 ID(12bit 序列号 × 1000);
- 优点:本地生成、无网络开销、趋势递增、可反解时间与机器(排查定位);
- 缺陷与解法:
- 时钟回拨(NTP 校正导致时间倒退)→ 序列号兜底/等待时钟追上/拒绝服务(百度 UidGenerator 用"未来时间"补偿);
- 机器号分配 → 手动配置/ZK 注册(Leaf-snowflake 用 ZK 分配机器 ID)。
java
// 雪花算法核心(简化)
long id = (timestamp - twepoch) << 22
| (workerId << 12)
| sequence;3.5 美团 Leaf 综合方案
| 模式 | 原理 | 适用 |
|---|---|---|
| Leaf-segment(号段) | DB 号段 + 双 buffer 预取 | 需要趋势递增(订单号、主键) |
| Leaf-snowflake(雪花) | 雪花 + ZK 分配机器号 + 时钟回拨兜底 | 需要高性能本地生成 |
4. 选型建议
text
简单场景(单应用、低并发) → 数据库自增/号段
高性能注册中心常有 → Redis INCR(可容忍丢号)或 自研雪花
分库分表主键 / 订单号 → 雪花算法(推荐)或 Leaf-segment
需要严格递增且可预测性无所谓 → 号段模式补充:业务 ID 与数据库主键分离——业务展示用分布式 ID,数据库内部仍可用自增(内部聚簇排序),但分库时必须全局唯一,故主流仍是分布式 ID 直接做主键。
5. 面试高频问题
- 为什么不用 UUID? 无序(索引页分裂)、长度大(存储膨胀)、不可排序;
- 雪花算法为什么会有时钟回拨问题? ID 依赖系统时间;NTP 回拨会导致生成重复 ID 或乱序;解法:等待/序列号兜底/拒绝;
- 号段模式和雪花各适合什么? 号段:趋势递增、可预测、依赖 DB 但可用性高;雪花:本地生成性能最高、需处理时钟回拨;
- 如何保证 ID 不泄露业务量? 随机化起始时间戳、加随机偏移、业务上避免连续可推导。
6. 小结
- 分布式 ID 五要求:唯一、递增、高性能、高可用、安全;
- 方案谱系:UUID(最简单)→ DB 自增/Redis(有依赖)→ 雪花(标配) → 号段(Leaf);
- 雪花 64 位结构:时间戳 + 机器号 + 序列号,本地生成、趋势递增、可反解;
- 核心风险点:时钟回拨(雪花)与可用性(DB/Redis 方案);
- 工业实践:美团 Leaf 双模式(号段 + 雪花)是完整参考。
下一章讲解分库分表后的扩容难题:数据迁移、双写与平滑扩容方案。