{T}

分布式 ID 生成方案

在分布式系统中,业务数据往往分散在多个数据库、多个服务节点中,传统的单库自增主键无法保证全局唯一性。因此需要一个全局唯一、且在多数场景下趋势递增的 ID 生成方案。本文系统讲解分布式 ID 的核心要求、常见生成方案及各自的取舍。

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

1.1 单库自增主键的局限

在单体应用中,数据库自增主键(AUTO_INCREMENT)简单可靠。但在分布式场景下:

  • 分库分表后:多个库/表各自维护自增序列,必然产生重复 ID。
  • 数据迁移与合并:不同库的自增 ID 无法保证全局唯一。
  • 主从复制:依赖数据库自增会增加主库压力,且需要额外的序列表同步。

1.2 分布式 ID 的硬性要求

要求说明原因
全局唯一整个系统内 ID 不重复作为主键/关联键的根本前提
趋势递增ID 大致随时间单调递增便于按主键排序、利于数据库索引(B+树顺序写)
高性能生成速度快,不成为瓶颈高并发下单、消息等场景高频调用
高可用生成服务不宕机、不降级ID 服务故障会导致写入链路中断
安全不暴露业务信息、不易被遍历防止通过 ID 猜测订单量、被爬取

注意区分"严格递增"与"趋势递增":严格递增需要全局串行(性能差);趋势递增允许不同节点之间有一定跳动,但整体单调,兼顾了性能与索引友好。

二、常见生成方案

2.1 UUID

UUID(Universally Unique Identifier)是一个 128 位的全局唯一标识,Java 中通过 UUID.randomUUID() 生成。

优点

  • 生成简单,无需任何外部依赖。
  • 本地生成,性能极高。
  • 天然全局唯一(基于时间戳 + 节点信息 + 随机数)。

缺点

  • 无序:随机 UUID 不递增,作为数据库主键会随机写,导致 B+ 树频繁分裂、页分裂,索引性能下降。
  • 太长:36 个字符(含连字符),占用存储空间大,不适合作为主键。
  • 不含业务含义。

结论:UUID 适合作为业务单据号分布式链路 traceId消息消息 ID 等不需要排序且对可读性要求低的场景,不适合作为数据库主键

2.2 数据库自增 / 序列

在独立的一张序列表上维护自增序列:

sql
CREATE TABLE `sequence` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `stub` char(1) NOT NULL DEFAULT '',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_stub` (`stub`)
) ENGINE=InnoDB;

-- 生成一个 ID
REPLACE INTO `sequence` (`stub`) VALUES ('b');
SELECT LAST_INSERT_ID();

优点:实现简单,ID 严格递增,绝对唯一。 缺点数据库单点瓶颈,高并发下成为热点,且依赖数据库可用性。

优化:号段模式(segment)——每次从数据库取一批 ID(如一批 1000 个),缓存在应用内存中,用完再取下一批。这样把对数据库的访问频率降低 1000 倍:

text
号段模式:
内存中持有 [1000, 1999] 这一段 ID
用完后请求数据库,获取下一段 [2000, 2999]
数据库只需记录 next_start(当前发到的号)

美团 Leaf-segment 即此思路,把数据库压力均摊到多台机器,性能可达 QPS 数万级。缺点是存在 ID 浪费(预取的段可能用不完)、以及 Leaf 重启后可能跳号。

2.3 雪花算法(Snowflake)

雪花算法是 Twitter 开源的一种分布式 ID 生成算法,生成的 ID 是一个 64 位 long,结构如下:

text
 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| unused(1bit) |   timestamp(41bit)   | workerId(10bit)| seq(12bit)|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  • 符号位(1 bit):始终为 0,保证 ID 为正数。
  • 时间戳(41 bit):毫秒级时间戳,可表示约 69 年。
  • 机器 ID(10 bit):标识不同节点,可部署 1024 台机器(也可拆分为 5bit datacenterId + 5bit workerId)。
  • 序列号(12 bit):同一毫秒内的递增序列,支持每毫秒 4096 个 ID。

生成逻辑

java
public class SnowflakeIdWorker {
  private final long workerId;
  private long sequence = 0L;
  private long lastTimestamp = -1L;

  public synchronized long nextId() {
    long timestamp = System.currentTimeMillis();
    if (timestamp < lastTimestamp) {
      // 时钟回拨处理(见下文)
    }
    if (timestamp == lastTimestamp) {
      sequence = (sequence + 1) & 4095;  // 12bit 序列
      if (sequence == 0) {                // 该毫秒序列耗尽
        timestamp = tilNextMillis(lastTimestamp);
      }
    } else {
      sequence = 0;
    }
    lastTimestamp = timestamp;
    return ((timestamp - START_EPOCH) << 22) | (workerId << 12) | sequence;
  }
}

优点

  • 趋势递增,对索引友好。
  • 性能极高,纯内存计算。
  • 不依赖数据库,天然高可用。
  • 可通过时间戳反推生成时间。

缺点:依赖机器时钟,时钟回拨会导致 ID 重复

时钟回拨的解决方案

  1. 回拨时间短(毫秒级):等待回拨的时间差后再生成,即 tilNextMillis 阻塞等待。
  2. 回拨较长:拒绝生成(抛出异常)或使用备用策略(如借助 ZooKeeper 选举新的 workerId)。
  3. 生产上通常配合小回拨等待 + 大回拨抛异常或切换节点

业界实现

  • 百度 UidGenerator:将 64 位拆为 sign + deltaSeconds(秒级)+ workerId + sequence,通过 workerId 的环形分配解决时钟回拨。
  • 美团 Leaf-snowflake:通过 ZooKeeper 注册 workerId,并对比本机时钟与 ZK 集群时钟,检测到回拨时拒绝生成。

2.4 基于 Redis 的 INCR

利用 Redis 的原子自增命令 INCR/INCRBY 生成自增 ID:

text
INCR order_id   // 返回递增后的值

优点:性能高、实现简单、可趋势递增。 缺点:依赖 Redis 可用性;需配合 INCRBY 和步长解决多实例冲突(不同实例用不同起始值 + 相同步长);Redis 若持久化丢失可能重复。

三、方案对比与选型

方案全局唯一趋势递增性能高可用缺点
UUID×极高无需依赖无序、太长,不适合主键
数据库自增√ 严格单点瓶颈高并发热点
号段模式√ 趋势依赖数据库(低频)有 ID 浪费/跳号
雪花算法√ 趋势极高不依赖外部依赖时钟,需处理回拨
Redis INCR依赖 Redis依赖可用性/持久化

选型建议

  • 日志、traceId、消息 ID → UUID(无需排序)。
  • 中小规模、数据库主键 → 号段模式(Leaf-segment)。
  • 大规模、高性能、趋势递增主键 → 雪花算法(Leaf-snowflake / 百度 UidGenerator),并配套时钟回拨治理。
  • 已有 Redis 基础设施、ID 要求不高 → Redis INCR。

小结

  • 分布式 ID 需满足全局唯一、趋势递增、高性能、高可用
  • UUID 简单但无序、过长;数据库自增严格递增但单点;号段模式用"一次取一批"缓解数据库压力;雪花算法是主流高性能方案,关键在于时钟回拨治理
  • 业界成熟方案:百度 UidGenerator、美团 Leaf(segment 与 snowflake 两种模式)。

版本差异(技术原理说明)

维度说明
技术原理分布式一致性/事务/锁/ID 生成等原理与具体版本无关,长期有效
落地选型新项目建议优先使用 Nacos/Redis/Seata 等成熟组件(JDK 17+ 兼容)
Java 版本示例代码基于 JDK 8 编写,JDK 17/21 下语法兼容

本文讲解的分布式系统核心问题与解决方案原理稳定,不随框架版本变化;落地时选用支持 JDK 17/21 的组件版本即可。