分布式 ID 生成方案——雪花算法改进版、Leaf、UidGenerator 实战对比
分布式 ID 生成方案——雪花算法改进版、Leaf、UidGenerator 实战对比
适读人群:需要在微服务中生成全局唯一 ID 的 Java 后端开发者 | 阅读时长:约16分钟 | 核心价值:掌握主流分布式 ID 方案的原理、优缺点和最佳实践
"时钟回拨"那个夜晚
两年前,我在一个做社交电商的公司,负责订单系统重构。上线两周后,某个深夜我收到了告警:订单 ID 生成失败,报错是 Clock moved backwards!。
查了一下,是我们用的雪花算法(Snowflake)检测到了系统时钟回拨。运维同学那边解释:那天服务器做了 NTP 时间同步,把时钟往回拨了 15 毫秒。就是这 15 毫秒,把雪花算法卡住了——程序直接抛异常,订单 ID 生成失败,用户下单报错。
这段时间有 2000 多个用户下单失败,全部需要重试,客服接了很多投诉。
事后复盘,我发现自己对雪花算法的实现细节理解不够深。重新研究了一遍,顺带把 Leaf 和 UidGenerator 也全部搞透了。
分布式 ID 的核心要求
在深入方案之前,先明确分布式 ID 必须满足的条件:
- 全局唯一:不同节点生成的 ID 不能重复
- 趋势递增:数据库使用 B+ Tree 索引,随机 ID 会导致大量索引分裂,性能差
- 高性能:ID 生成不能成为系统瓶颈,需要支持每秒百万级
- 高可用:ID 生成服务不能有单点故障
- 信息安全:ID 不应暴露业务信息(如不应可以通过 ID 推算订单量)
方案一:原版雪花算法与时钟回拨问题
64 位结构解析
0 - 41位时间戳 - 10位机器ID - 12位序列号
│ │ │ │
│ │ └── 最多 1024 个节点
│ └── 精确到毫秒,可使用 69 年
└── 固定为 0(保证 ID 是正数)public class SnowflakeIdGenerator {
// 起始时间戳(2020-01-01 00:00:00)
private static final long START_TIMESTAMP = 1577836800000L;
// 各部分位数
private static final long SEQUENCE_BITS = 12L;
private static final long MACHINE_ID_BITS = 5L;
private static final long DATA_CENTER_BITS = 5L;
// 最大值
private static final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BITS); // 4095
private static final long MAX_MACHINE_ID = ~(-1L << MACHINE_ID_BITS); // 31
private static final long MAX_DATA_CENTER_ID = ~(-1L << DATA_CENTER_BITS); // 31
// 位移量
private static final long MACHINE_ID_SHIFT = SEQUENCE_BITS;
private static final long DATA_CENTER_SHIFT = SEQUENCE_BITS + MACHINE_ID_BITS;
private static final long TIMESTAMP_SHIFT = DATA_CENTER_SHIFT + DATA_CENTER_BITS;
private final long machineId;
private final long dataCenterId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public SnowflakeIdGenerator(long dataCenterId, long machineId) {
if (dataCenterId > MAX_DATA_CENTER_ID || dataCenterId < 0) {
throw new IllegalArgumentException("数据中心 ID 超出范围");
}
if (machineId > MAX_MACHINE_ID || machineId < 0) {
throw new IllegalArgumentException("机器 ID 超出范围");
}
this.machineId = machineId;
this.dataCenterId = dataCenterId;
}
public synchronized long nextId() {
long currentTimestamp = System.currentTimeMillis();
// 时钟回拨处理
if (currentTimestamp < lastTimestamp) {
long offset = lastTimestamp - currentTimestamp;
if (offset <= 5) {
// 回拨小于5ms,等待追上来
try {
Thread.sleep(offset << 1);
currentTimestamp = System.currentTimeMillis();
if (currentTimestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨,无法生成 ID");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("时钟等待被中断");
}
} else {
// 回拨超过5ms,直接报错(由调用方决定如何处理)
throw new RuntimeException(
String.format("时钟回拨超过 %d ms,拒绝生成 ID", offset));
}
}
// 同一毫秒内的序列号递增
if (lastTimestamp == currentTimestamp) {
sequence = (sequence + 1) & MAX_SEQUENCE;
if (sequence == 0) {
// 序列号用完,等待下一毫秒
currentTimestamp = waitNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = currentTimestamp;
return ((currentTimestamp - START_TIMESTAMP) << TIMESTAMP_SHIFT)
| (dataCenterId << DATA_CENTER_SHIFT)
| (machineId << MACHINE_ID_SHIFT)
| sequence;
}
private long waitNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
}机器 ID 自动分配
@Component
public class SnowflakeAutoConfigService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private SnowflakeProperties properties;
private SnowflakeIdGenerator idGenerator;
@PostConstruct
public void init() {
long machineId = allocateMachineId();
this.idGenerator = new SnowflakeIdGenerator(
properties.getDataCenterId(), machineId);
log.info("雪花算法初始化完成,dataCenterId={}, machineId={}",
properties.getDataCenterId(), machineId);
}
/**
* 从 Redis 分配机器 ID(0-31)
* 启动时注册,服务停止时注销
*/
private long allocateMachineId() {
String instanceId = getLocalIP() + ":" + getServerPort();
String redisKey = "snowflake:machine_id:" + instanceId;
// 检查是否已有分配的 ID
String existingId = redisTemplate.opsForValue().get(redisKey);
if (existingId != null) {
return Long.parseLong(existingId);
}
// 在 0-31 中找一个可用的 ID
for (int i = 0; i <= 31; i++) {
String machineKey = "snowflake:machine:" + i;
Boolean success = redisTemplate.opsForValue()
.setIfAbsent(machineKey, instanceId, Duration.ofDays(1));
if (Boolean.TRUE.equals(success)) {
// 注册成功
redisTemplate.opsForValue()
.set(redisKey, String.valueOf(i), Duration.ofDays(1));
// 启动心跳续期
startHeartbeat(machineKey, i);
return i;
}
}
throw new RuntimeException("机器 ID 分配失败,所有 ID 已被占用");
}
private void startHeartbeat(String machineKey, long machineId) {
// 每12小时续期一次(有效期1天)
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.scheduleAtFixedRate(() -> {
redisTemplate.expire(machineKey, Duration.ofDays(1));
}, 12, 12, TimeUnit.HOURS);
}
}方案二:美团 Leaf
Leaf 是美团开源的分布式 ID 生成服务,提供两种模式:
Leaf-segment(号段模式)
号段模式的核心思路:从数据库批量取号,缓存在内存中,按需分配。
CREATE TABLE leaf_alloc (
biz_tag VARCHAR(128) NOT NULL COMMENT '业务标签',
max_id BIGINT NOT NULL DEFAULT 1 COMMENT '当前最大ID',
step INT NOT NULL COMMENT '每次分配的号段步长',
description VARCHAR(256) DEFAULT NULL,
update_time TIMESTAMP NOT NULL,
PRIMARY KEY (biz_tag)
);
-- 初始化订单号段
INSERT INTO leaf_alloc(biz_tag, max_id, step, description)
VALUES('order_id', 1, 2000, '订单ID');@Service
public class LeafSegmentIdService {
@Autowired
private LeafAllocMapper leafAllocMapper;
// 每个业务标签的号段缓存
private final Map<String, SegmentBuffer> cache = new ConcurrentHashMap<>();
public long nextId(String bizTag) {
SegmentBuffer buffer = cache.computeIfAbsent(bizTag,
k -> new SegmentBuffer(k));
return buffer.next();
}
}
/**
* 双 Buffer:一个在用,一个在预加载,保证不中断
*/
public class SegmentBuffer {
private final String bizTag;
private final Segment[] segments = new Segment[2];
private volatile int currentSegment = 0;
private volatile boolean nextReady = false;
// 当当前 Segment 使用了 10% 时,开始预加载下一个 Segment
// 避免号段用完时等待数据库响应
public synchronized long next() {
Segment current = segments[currentSegment];
// 使用量超过 10%,异步预加载下一个号段
if (!nextReady && current.getIdle() < current.getStep() * 0.9) {
asyncLoadNextSegment();
}
long id = current.getValue().getAndIncrement();
if (id < current.getMax()) {
return id;
}
// 当前号段用完,切换到下一个
waitAndSwitchSegment();
return next();
}
private void asyncLoadNextSegment() {
// 异步向数据库申请新号段
CompletableFuture.runAsync(() -> {
// UPDATE leaf_alloc SET max_id = max_id + step WHERE biz_tag = ?
// 返回新的 [max_id - step, max_id) 区间
});
}
}Leaf-segment 的优势:
- 每次批量取 2000 个号,数据库压力极小
- 双 Buffer 保证服务不中断
- 号段用完的速度可监控
局限: ID 能暴露业务量(可以根据 ID 推算大约多少订单)。
Leaf-snowflake 模式
Leaf 的雪花模式改进了机器 ID 的管理:使用 ZooKeeper 自动分配机器 ID,并在本地缓存,防止 ZK 不可用时 ID 生成失败。
方案三:百度 UidGenerator
UidGenerator 是百度开源的另一种雪花算法实现,最大的特点是用数据库分配机器 ID,并且针对 Java 容器(如 Tomcat)的特性做了优化。
// UidGenerator 使用示例
@Service
public class UidGeneratorService {
@Resource
private UidGenerator uidGenerator;
public long nextId() {
return uidGenerator.getUID();
}
public String parseId(long uid) {
return uidGenerator.parseUID(uid);
// 输出类似:{ "UID":"180539788350046209", "timestamp":"2019-02-20 14:55:29",
// "workerId":"27", "sequence":"1" }
}
}UidGenerator 的创新点是环形缓冲区(RingBuffer):
- 后台线程预先生成一批 ID,存入 RingBuffer
- 取 ID 时从 RingBuffer 直接读,无锁、极高性能(单机 600 万+/s)
- RingBuffer 消耗到一定程度时,后台自动补充
三大踩坑实录
坑一:雪花算法在 K8s 上机器 ID 冲突
现象: 服务部署在 K8s,Pod IP 会变化,用 IP 末段作为机器 ID 不唯一,发现有重复 ID,数据库写入报唯一键冲突。
原因: K8s Pod 重启后 IP 可能不变,但如果多个 Pod 同时运行且 IP 末段相同,就会冲突。
解法: 不用 IP 做机器 ID,改用 Redis 或 ZooKeeper 动态分配。每个 Pod 启动时向 Redis 申请一个唯一的机器 ID,关闭时释放。
坑二:Leaf-segment 号段耗尽时的瞬间延迟
现象: Leaf 号段模式下,偶发性出现 ID 生成耗时突然从 0.1ms 跳到 100ms+,持续约 1 秒,期间大量请求超时。
原因: 双 Buffer 的触发条件(10% 阈值)在高并发下,10% 的号段可能在预加载完成之前就全部消耗完了,导致请求等待数据库返回新号段。
解法: 调整触发预加载的阈值:从 10% 调整到 20%(即消耗了 80% 时就开始预加载),给预加载留更多时间。也可以增大步长(step),让每个号段使用更久。
坑三:时间戳位数不够,69 年后 ID 溢出
现象: 这还没发生,但技术评审时被质疑。
原因: 雪花算法的 41 位时间戳可以用 69 年,如果从 2020 年开始,到 2089 年会溢出。对于大多数公司,69 年够用了,但如果你把起始时间戳设的很靠前(比如 Unix 时间戳 0),那可能用不了那么久。
解法: 把起始时间戳设为服务上线时间,而不是 Unix 纪元。另外可以考虑用 48 位时间戳(可用约 8925 年),减少机器 ID 或序列号的位数来换。
各方案综合对比
| 维度 | 原版雪花 | Leaf-segment | Leaf-snowflake | UidGenerator |
|---|---|---|---|---|
| 性能 | 100万+/s | 几十万/s | 100万+/s | 600万+/s |
| 依赖 | 无 | MySQL | ZooKeeper | MySQL |
| 时钟回拨 | 有风险 | 无关 | 有改进 | 无关 |
| 趋势递增 | 是 | 是 | 是 | 是 |
| 可读性 | 差(纯数字) | 差 | 差 | 差 |
| 实现复杂度 | 低 | 中 | 中 | 中 |
我的推荐:
- 简单场景(< 10 万 TPS):自己实现雪花算法 + Redis 分配机器 ID
- 中等场景:Leaf-segment,依赖 MySQL 但稳定,美团用了多年
- 超高性能场景:UidGenerator,单机 600 万+/s
