在分布式系统中,生成全局唯一 ID 是一个看似简单、实则深坑无数的基础问题。
“不就是生成个 ID 吗?用 UUID.randomUUID() 不就行了?”
如果你在面试或架构评审中这么说,大概率会被“请”出去。
为什么?UUID 无序、太长、会导致数据库页分裂……
那么,数据库自增 ID 呢?分库分表后怎么办?
雪花算法(Snowflake)?时钟回拨问题怎么解?
今天,我们不讲虚的,用万字长文(夸张修辞,实则干货满满)带你彻底打通分布式 ID 生成的任督二脉。从原理到源码,从方案选型到生产级落地,一篇管饱!
🎯 核心需求:我们需要什么样的 ID?
一个优秀的分布式 ID,必须满足以下 “四大金刚” :
- 全局唯一:这是最基本的,不能重复。
- 趋势递增:这对数据库索引(尤其是 MySQL InnoDB)非常重要,能减少页分裂,提升写入性能。
- 高性能:生成 ID 的动作不能成为系统的瓶颈,延迟要低,吞吐要高。
- 高可用:ID 生成服务挂了,整个系统就瘫痪了,所以必须 5 个 9 (99.999%) 可用。
加分项:
- 信息安全:ID 最好不要直接暴露业务量(比如不要连续自增,否则竞品一下就知道你每天有多少订单)。
- 短小精悍:存储空间越小越好。
⚔️ 方案演进之路
1. ❌ 方案一:UUID (Java 自带)
String id = UUID.randomUUID().toString().replace("-", "");
// 结果:c2b8c2b8c2b8c2b8c2b8c2b8c2b8c2b8
-
优点:本地生成,性能无敌,没有网络消耗。
-
缺点:
- 太长:32 位 16 进制字符串,占用空间大。
- 无序:完全随机,导致 MySQL B+ 树索引频繁分裂,写入性能极差(这是致命伤)。
- 不可读:无法从 ID 中获取时间、业务等信息。
-
结论:坚决不用做主键,只适合做 Token 或文件名。
2. ⚠️ 方案二:数据库自增 ID (MySQL)
利用 MySQL 的 auto_increment 特性。
-
优点:简单,单调递增,查询快。
-
缺点:
- 单点故障:数据库挂了就完了。
- 性能瓶颈:单机数据库写入能力有限。
- 分库分表麻烦:不同库生成的 ID 会重复。
-
改进版:号段模式 (Leaf-segment)
- 原理:不每次都去数据库取 ID,而是去取一个“号段”(比如一次取 1000 个)。业务服务在内存里慢慢发,发完了再去取下一个号段。
- 优点:大大减轻数据库压力。
- 缺点:ID 是连续的,容易暴露业务量;服务重启会浪费 ID。
3. ✅ 方案三:雪花算法 (Snowflake) —— 业界标杆
这是 Twitter 开源的算法,也是目前最主流的方案。它生成的是一个 64 位的 Long 型整数。
结构拆解 (64 bit) :
- 1 bit:不使用 (符号位)。
- 41 bits:毫秒级时间戳 (可以使用 69 年)。
- 10 bits:机器 ID (5位数据中心 ID + 5位工作机器 ID,支持 1024 个节点)。
- 12 bits:毫秒内的序列号 (每毫秒产生 4096 个 ID)。
理论吞吐量:4096 * 1000 = 400 万 ID/秒。
🛠️ 手写一个生产级 Snowflake (解决时钟回拨)
雪花算法最大的坑是 “时钟回拨” 。如果服务器时间回调了,可能会生成重复 ID。
下面的代码是一个改良版,增加了时钟回拨的检测和处理。
/**
* 生产级雪花算法实现
*/
public class SnowflakeIdGenerator {
// 起始时间戳 (例如:2024-01-01 00:00:00)
private final long twepoch = 1704067200000L;
// 各部分占用的位数
private final long workerIdBits = 5L;
private final long datacenterIdBits = 5L;
private final long sequenceBits = 12L;
// 各部分最大值 (通过位移计算)
private final long maxWorkerId = -1L ^ (-1L << workerIdBits); // 31
private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); // 31
private final long sequenceMask = -1L ^ (-1L << sequenceBits); // 4095
// 各部分左移位数
private final long workerIdShift = sequenceBits;
private final long datacenterIdShift = sequenceBits + workerIdBits;
private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
private long workerId;
private long datacenterId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public SnowflakeIdGenerator(long workerId, long datacenterId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
public synchronized long nextId() {
long timestamp = timeGen();
// 🚨 核心逻辑:时钟回拨处理
if (timestamp < lastTimestamp) {
long offset = lastTimestamp - timestamp;
if (offset <= 5) {
// 如果回拨时间很短(<=5ms),等待追上来
try {
wait(offset << 1);
timestamp = timeGen();
if (timestamp < lastTimestamp) {
throw new RuntimeException("Clock moved backwards. Refusing to generate id");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} else {
// 回拨过大,直接报错,或者切换 workerId (生产环境建议报警)
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
}
// 如果是同一毫秒生成的
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
// 毫秒内序列溢出 (超过4095)
if (sequence == 0) {
// 阻塞到下一毫秒
timestamp = tilNextMillis(lastTimestamp);
}
} else {
// 新的一毫秒,序列号重置
sequence = 0L;
}
lastTimestamp = timestamp;
// 拼接 ID
return ((timestamp - twepoch) << timestampLeftShift) |
(datacenterId << datacenterIdShift) |
(workerId << workerIdShift) |
sequence;
}
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
private long timeGen() {
return System.currentTimeMillis();
}
}
🏢 大厂开源方案剖析
如果不想自己造轮子,可以直接用大厂的开源组件,它们解决了很多边缘问题。
1. 美团 Leaf
Leaf 提供了两种模式:
- Leaf-segment (号段模式) :基于 MySQL,性能高,但 ID 递增。
- Leaf-snowflake (雪花模式) :基于 ZooKeeper 持久化节点来分配 WorkerID,解决了 WorkerID 难以管理的问题。同时对时钟回拨做了非常完善的校验。
2. 百度 UidGenerator
- 特点:基于 Snowflake,但打破了传统位数分配。
- RingBuffer 缓存:它使用 RingBuffer 缓存生成的 ID,吞吐量极高(压测可达 600万 QPS)。
- 缺点:依赖 MySQL,配置稍显复杂。
💡 架构师的最终建议
在做技术选型时,不要盲目追求“最强”,要追求“最合适”。
-
中小项目 / 单体应用:
- 直接用 MyBatis-Plus 自带的 IdWorker (基于 Snowflake)。开箱即用,不用写一行代码。
- 配置:@TableId(type = IdType.ASSIGN_ID)。
-
分库分表 / 微服务初期:
- 使用上面的 手写 Snowflake 代码,或者引入 Hutool 工具包 的 IdUtil.getSnowflake()。
- 注意:要保证每个服务的 workerId 和 datacenterId 不重复(可以通过 Redis 或环境变量动态分配)。
-
超高并发 / 核心金融系统:
- 上 美团 Leaf。它经过了美团内部万亿级流量的考验,稳定性极强,且有完善的监控和容灾机制。
📝 总结
| 方案 | 全局唯一 | 趋势递增 | 性能 | 复杂度 | 适用场景 |
|---|---|---|---|---|---|
| UUID | ✅ | ❌ | ⭐⭐⭐⭐⭐ | ⭐ | 临时 Token、文件名 |
| MySQL 自增 | ✅ | ✅ | ⭐⭐ | ⭐ | 小规模单体应用 |
| Snowflake | ✅ | ✅ | ⭐⭐⭐⭐ | ⭐⭐ | 绝大多数互联网业务 |
| 美团 Leaf | ✅ | ✅ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | 大厂核心业务、高并发 |
希望这篇深度长文能帮你彻底搞定分布式 ID!下次面试官问你,直接把这篇文章的逻辑甩给他!😎