一句话总结
分布式ID要求:全局唯一、趋势递增(利于索引)、高性能、高可用。方案对比:UUID(简单但无序,索引性能差)→ Snowflake(64位:1+41时间戳+10机器+12序列号,趋势递增,最常用)→ 号段模式(美团Leaf,预分配号段,性能最高)→ Redis自增(简单但依赖Redis)。Snowflake核心挑战是时钟回拨,解决方案:等待、抛异常、或使用号段模式。
初级理解
方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| UUID | 简单、本地生成 | 无序(索引性能差)、太长 | 小规模、非主键 |
| Snowflake | 趋势递增、高性能 | 依赖时钟、需分配机器ID | 大多数场景(推荐) |
| 号段模式 | 性能最高、无时钟依赖 | 实现复杂、ID不连续 | 超高并发 |
| Redis自增 | 简单、严格递增 | 依赖Redis、单点故障 | 小规模 |
一句话总结:UUID 简单但无序;Snowflake 最常用;号段模式性能最高。
中级深入
Snowflake 雪花算法
# Snowflake 64位结构
# 1 位:符号位(始终为 0)
# 41 位:时间戳(毫秒级,可用 69 年)
# 10 位:工作机器 ID(5 位数据中心 + 5 位机器 ID,最多 1024 台)
# 12 位:序列号(同一毫秒内最多 4096 个)
public class SnowflakeIdGenerator {
private final long workerId;
private final long datacenterId;
private long sequence = 0L;
private long lastTimestamp = -1L;
private static final long EPOCH = 1700000000000L; # 起始时间戳
public synchronized long nextId() {
long timestamp = System.currentTimeMillis();
# 时钟回拨处理
if (timestamp < lastTimestamp) {
throw new RuntimeException("Clock moved backwards!");
}
if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & 0xFFF; # 4095
if (sequence == 0) {
timestamp = waitNextMillis(lastTimestamp);
}
} else {
sequence = 0;
}
lastTimestamp = timestamp;
return ((timestamp - EPOCH) << 22)
| (datacenterId << 17)
| (workerId << 12)
| sequence;
}
}
时钟回拨解决方案
# 时钟回拨:NTP 时间同步导致系统时间倒退
# 解决方案:
# 1. 等待(简单)
# 如果回拨时间短(如 < 5ms),等待追上
private long waitNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
# 2. 抛异常(严格)
# 回拨就拒绝服务,等时钟恢复
# 3. 备用序列号(推荐)
# 记录上次时间戳,回拨时使用备用 workerId
# 或使用扩展位(借用未来时间戳的位)
# 4. 号段模式(根本解决)
# 不依赖时钟,从 DB 预分配号段
中级要点:Snowflake 64位 = 1+41+10+12;时钟回拨可等待、抛异常或备用序列号。
高级拓展
号段模式(美团 Leaf)
# 号段模式原理
# 1. 从 DB 批量获取号段(如 1~1000)
# 2. 本地内存中分配 ID
# 3. 号段用完再取下一段
# 优点:
# 1. 性能极高(本地分配,无网络开销)
# 2. 无时钟依赖
# 3. DB 挂了也能撑一段时间
# 双 Buffer 优化
# buffer1: 当前号段(正在分配)
# buffer2: 预加载号段(异步加载)
# buffer1 用完 → 切换到 buffer2 → 异步加载新 buffer1
# DB 表设计
CREATE TABLE id_segment (
biz_tag VARCHAR(50) PRIMARY KEY,
max_id BIGINT NOT NULL,
step INT NOT NULL DEFAULT 1000
);
# 获取号段
UPDATE id_segment SET max_id = max_id + step
WHERE biz_tag = 'order';
SELECT max_id FROM id_segment WHERE biz_tag = 'order';
实战场景
场景:订单ID生成
# 订单ID要求
# 1. 全局唯一
# 2. 趋势递增(利于 MySQL 索引)
# 3. 包含业务信息(可选)
# 方案:Snowflake + 业务前缀
# 订单ID = 业务前缀 + Snowflake ID
# 如:ORD1700000000001
# 使用 Hutool 工具类
long id = IdUtil.getSnowflake(1, 1).nextId();
String orderId = "ORD" + id;
面试模拟
面试官:分布式ID有哪些方案?
你:1)UUID:简单但无序,索引性能差;2)Snowflake:64位,1+41+10+12,趋势递增,最常用;3)号段模式(美团Leaf):预分配号段,性能最高,无时钟依赖;4)Redis自增:简单但依赖Redis。推荐Snowflake或号段模式。
面试官:Snowflake 时钟回拨怎么解决?
你:1)回拨时间短(<5ms):等待追上;2)回拨时间长:抛异常或使用备用workerId;3)根本解决:用号段模式(不依赖时钟)。生产环境建议用号段模式或做好NTP配置防止大幅回拨。