分布式 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 数据库自增 / 序列
在独立的一张序列表上维护自增序列:
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 倍:
号段模式:
内存中持有 [1000, 1999] 这一段 ID
用完后请求数据库,获取下一段 [2000, 2999]
数据库只需记录 next_start(当前发到的号)美团 Leaf-segment 即此思路,把数据库压力均摊到多台机器,性能可达 QPS 数万级。缺点是存在 ID 浪费(预取的段可能用不完)、以及 Leaf 重启后可能跳号。
2.3 雪花算法(Snowflake)
雪花算法是 Twitter 开源的一种分布式 ID 生成算法,生成的 ID 是一个 64 位 long,结构如下:
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。
生成逻辑:
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 重复。
时钟回拨的解决方案:
- 回拨时间短(毫秒级):等待回拨的时间差后再生成,即
tilNextMillis阻塞等待。 - 回拨较长:拒绝生成(抛出异常)或使用备用策略(如借助 ZooKeeper 选举新的 workerId)。
- 生产上通常配合小回拨等待 + 大回拨抛异常或切换节点。
业界实现:
- 百度 UidGenerator:将 64 位拆为 sign + deltaSeconds(秒级)+ workerId + sequence,通过 workerId 的环形分配解决时钟回拨。
- 美团 Leaf-snowflake:通过 ZooKeeper 注册 workerId,并对比本机时钟与 ZK 集群时钟,检测到回拨时拒绝生成。
2.4 基于 Redis 的 INCR
利用 Redis 的原子自增命令 INCR/INCRBY 生成自增 ID:
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 的组件版本即可。