在分布式系统中,业务数据往往分散在多个数据库、多个服务节点中,传统的单库自增主键无法保证全局唯一性。因此需要一个全局唯一、且在多数场景下趋势递增的 ID 生成方案。本文系统讲解分布式 ID 的核心要求、常见生成方案及各自的取舍。
在单体应用中,数据库自增主键(AUTO_INCREMENT)简单可靠。但在分布式场景下:
| 要求 | 说明 | 原因 |
|---|---|---|
| 全局唯一 | 整个系统内 ID 不重复 | 作为主键/关联键的根本前提 |
| 趋势递增 | ID 大致随时间单调递增 | 便于按主键排序、利于数据库索引(B+树顺序写) |
| 高性能 | 生成速度快,不成为瓶颈 | 高并发下单、消息等场景高频调用 |
| 高可用 | 生成服务不宕机、不降级 | ID 服务故障会导致写入链路中断 |
| 安全 | 不暴露业务信息、不易被遍历 | 防止通过 ID 猜测订单量、被爬取 |
注意区分"严格递增"与"趋势递增":严格递增需要全局串行(性能差);趋势递增允许不同节点之间有一定跳动,但整体单调,兼顾了性能与索引友好。
UUID(Universally Unique Identifier)是一个 128 位的全局唯一标识,Java 中通过 UUID.randomUUID() 生成。
优点:
缺点:
结论:UUID 适合作为业务单据号、分布式链路 traceId、消息 ID 等不需要排序且对可读性要求低的场景,不适合作为数据库主键。
在独立的一张序列表上维护自增序列:
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 重启后可能跳号。
雪花算法是 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)|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+生成逻辑:
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 阻塞等待。业界实现:
利用 Redis 的原子自增命令 INCR/INCRBY 生成自增 ID:
INCR order_id // 返回递增后的值优点:性能高、实现简单、可趋势递增。
缺点:依赖 Redis 可用性;需配合 INCRBY 和步长解决多实例冲突(不同实例用不同起始值 + 相同步长);Redis 若持久化丢失可能重复。
| 方案 | 全局唯一 | 趋势递增 | 性能 | 高可用 | 缺点 |
|---|---|---|---|---|---|
| UUID | √ | × | 极高 | 无需依赖 | 无序、太长,不适合主键 |
| 数据库自增 | √ | √ 严格 | 低 | 单点瓶颈 | 高并发热点 |
| 号段模式 | √ | √ 趋势 | 高 | 依赖数据库(低频) | 有 ID 浪费/跳号 |
| 雪花算法 | √ | √ 趋势 | 极高 | 不依赖外部 | 依赖时钟,需处理回拨 |
| Redis INCR | √ | √ | 高 | 依赖 Redis | 依赖可用性/持久化 |
选型建议:
| 维度 | 说明 |
|---|---|
| 技术原理 | 分布式一致性/事务/锁/ID 生成等原理与具体版本无关,长期有效 |
| 落地选型 | 新项目建议优先使用 Nacos/Redis/Seata 等成熟组件(JDK 17+ 兼容) |
| Java 版本 | 示例代码基于 JDK 8 编写,JDK 17/21 下语法兼容 |
本文讲解的分布式系统核心问题与解决方案原理稳定,不随框架版本变化;落地时选用支持 JDK 17/21 的组件版本即可。