分布式 ID 生成
问题
分布式系统中如何生成全局唯一 ID?有哪些方案?雪花算法的原理是什么?
答案
分布式 ID 的核心要求
| 要求 | 说明 |
|---|---|
| 全局唯一 | 不同节点不会生成重复 ID |
| 趋势递增 | 有利于数据库索引性能(B+ 树顺序插入) |
| 高性能 | 生成速度快,不成为瓶颈 |
| 高可用 | ID 生成服务不能单点故障 |
| 信息安全 | 不暴露业务量等敏感信息(可选) |
方案对比
| 方案 | 唯一性 | 有序性 | 性能 | 可用性 | 适用场景 |
|---|---|---|---|---|---|
| UUID | ✅ | ❌ | 高 | 高 | 无排序需求,如 traceId |
| 数据库自增 | ✅ | ✅ | 低 | 低 | 单库小规模 |
| 数据库号段 | ✅ | ✅ | 高 | 中 | 中等规模 |
| Redis INCR | ✅ | ✅ | 高 | 中 | 需要严格递增 |
| 雪花算法 | ✅ | ✅ | 极高 | 高 | 主流方案 |
| Leaf(美团) | ✅ | ✅ | 高 | 高 | 大规模生产 |
| Tinyid(滴滴) | ✅ | ✅ | 高 | 高 | 号段模式优化 |
雪花算法(Snowflake)
Twitter 开源的分布式 ID 生成算法,生成 64 位 long 型 ID:
0 | 0000000000 0000000000 0000000000 0000000000 0 | 00000 | 00000 | 000000000000
↑ |←————————————— 41 位时间戳 ——————————————→|←5位→|←5位→|←——— 12 位序列号 ———→|
符号位 毫秒级时间戳 数据中心 机器ID 同一毫秒内自增序列
| 部分 | 位数 | 说明 |
|---|---|---|
| 符号位 | 1 | 固定 0(正数) |
| 时间戳 | 41 | 毫秒级,可用约 69 年 |
| 数据中心 ID | 5 | 0~31,共 32 个数据中心 |
| 机器 ID | 5 | 0~31,每个数据中心 32 台机器 |
| 序列号 | 12 | 同一毫秒内自增,0~4095 |
理论 QPS:单节点每毫秒可生成 4096 个 ID → 约 409.6 万/秒。
雪花算法实现
public class SnowflakeIdGenerator {
private final long epoch = 1704067200000L; // 自定义起始时间 2024-01-01
private final long dataCenterIdBits = 5L;
private final long workerIdBits = 5L;
private final long sequenceBits = 12L;
private final long workerIdShift = sequenceBits; // 12
private final long dataCenterIdShift = sequenceBits + workerIdBits; // 17
private final long timestampShift = sequenceBits + workerIdBits + dataCenterIdBits; // 22
private final long sequenceMask = ~(-1L << sequenceBits); // 4095
private final long dataCenterId;
private final long workerId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public SnowflakeIdGenerator(long dataCenterId, long workerId) {
this.dataCenterId = dataCenterId;
this.workerId = workerId;
}
public synchronized long nextId() {
long timestamp = System.currentTimeMillis();
// 时钟回拨检查
if (timestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨,拒绝生成ID: "
+ (lastTimestamp - timestamp) + "ms");
}
if (timestamp == lastTimestamp) {
// 同一毫秒,序列号自增
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
// 序列号溢出,等待下一毫秒
timestamp = waitNextMillis(lastTimestamp);
}
} else {
sequence = 0L; // 新毫秒,序列号重置
}
lastTimestamp = timestamp;
return ((timestamp - epoch) << timestampShift)
| (dataCenterId << dataCenterIdShift)
| (workerId << workerIdShift)
| sequence;
}
private long waitNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
}
雪花算法的时钟回拨问题
服务器 NTP 时间同步可能导致时钟回拨,生成重复 ID。
解决方案:
- 拒绝生成:回拨时直接抛异常(简单粗暴)
- 等待追上:回拨幅度小时(< 5ms)自旋等待
- 备用机器位:预留扩展位,回拨时切换到备用 workerId
- 美团 Leaf:基于 ZooKeeper 管理 workerId,回拨时上报告警
号段模式(数据库 + 内存缓存)
号段表
CREATE TABLE id_alloc (
biz_tag VARCHAR(64) PRIMARY KEY, -- 业务标签
max_id BIGINT NOT NULL, -- 当前最大 ID
step INT NOT NULL, -- 每次获取的号段长度
update_time TIMESTAMP
);
-- 获取号段
UPDATE id_alloc SET max_id = max_id + step WHERE biz_tag = 'order';
美团 Leaf 的号段模式使用 双 Buffer 优化:当前号段消耗到一定比例时异步预加载下一号段,实现无阻塞 ID 分配。
常见面试问题
Q1: 雪花算法有什么问题?如何解决?
答案:
| 问题 | 解决方案 |
|---|---|
| 时钟回拨 | 检测回拨后等待或切换 workerId |
| 机器 ID 分配 | ZooKeeper 自动分配 / 配置中心管理 |
| ID 不连续 | 业务无关紧要;如需连续用号段模式 |
| 可反解 | ID 可解析出时间戳和机器信息(不安全可加密) |
Q2: UUID 为什么不适合做数据库主键?
答案:
- 无序:UUID 是随机的,插入 B+ 树索引时会导致页分裂,严重降低写入性能
- 长度大:UUID 128 位(String 36 字符),比 long(8 字节)占用更多存储和索引空间
- 无业务含义:无法从 ID 中推导出时间等信息
- 不可读:不方便调试和排查
UUID 适合做分布式唯一标识(如 traceId、sessionId),不适合做数据库主键。
Q3: 号段模式和雪花算法怎么选?
答案:
| 对比 | 雪花算法 | 号段模式 |
|---|---|---|
| ID 格式 | 64位 long | 自增数字 |
| 依赖 | 无(本地生成) | 数据库 |
| 趋势递增 | 近似递增 | 严格递增 |
| 性能 | 极高(纯内存) | 高(号段缓存) |
| 信息泄露 | 可解析出时间和机器 | 可推算出业务量 |
| 适用 | 大多数场景 | 需要严格递增 |
Q4: 分库分表后的 ID 怎么生成?
答案:
分库分表后不能用单表自增 ID(不同表可能生成相同 ID),需要全局唯一 ID 方案。常见选择:
- 雪花算法:最常用,性能高,各节点独立生成
- 号段模式:如美团 Leaf,按业务 tag 分配号段
- ShardingSphere 内置:支持雪花算法和 UUID 两种策略
详见 分库分表。