{T}

存储拆分后,如何解决唯一主键问题?

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. 面试高频问题

  1. 为什么不用 UUID? 无序(索引页分裂)、长度大(存储膨胀)、不可排序;
  2. 雪花算法为什么会有时钟回拨问题? ID 依赖系统时间;NTP 回拨会导致生成重复 ID 或乱序;解法:等待/序列号兜底/拒绝;
  3. 号段模式和雪花各适合什么? 号段:趋势递增、可预测、依赖 DB 但可用性高;雪花:本地生成性能最高、需处理时钟回拨;
  4. 如何保证 ID 不泄露业务量? 随机化起始时间戳、加随机偏移、业务上避免连续可推导。

6. 小结

  • 分布式 ID 五要求:唯一、递增、高性能、高可用、安全
  • 方案谱系:UUID(最简单)→ DB 自增/Redis(有依赖)→ 雪花(标配) → 号段(Leaf);
  • 雪花 64 位结构:时间戳 + 机器号 + 序列号,本地生成、趋势递增、可反解;
  • 核心风险点:时钟回拨(雪花)与可用性(DB/Redis 方案);
  • 工业实践:美团 Leaf 双模式(号段 + 雪花)是完整参考。

下一章讲解分库分表后的扩容难题:数据迁移、双写与平滑扩容方案。