雪花算法 ≠ 绝对唯一,只能做到「在正确配置 + 不越界使用」前提下高度概率唯一;一旦时钟回拨或 workId 重复,就可能重复。下面给出“为什么不能 100 %” 和 “生产级优化方案”。
一、雪花 ID 结构(Snowflake-64bit)
0 符号位 | 41bit 时间戳(毫秒) | 10bit workId | 12bit 序列号- 时间精度 1 ms → 69 年范围
- 10 bit workId → 最多 1024 个节点
- 12 bit 序列号 → 每节点每毫秒 4096 个 ID
二、绝对唯一的 3 个天敌
- 时钟回拨(NTP、人工改时间)
同一毫秒时间被重复使用 → 时间戳相同 + workId 相同 + 序列号归零 → ID 重复。 - workId 重复(容器重启、K8s Pod IP 复用)**
两台机被分配相同 workId,同一毫秒序列号空间完全重叠。 - 序列号溢出(>4096/ms)**
单节点突刺 QPS > 4 k/ms 会等待或抛异常,若直接归零也重复。
三、生产级优化(“让概率降到宇宙级别”)
| 优化点 | 做法 | 效果 |
|---|---|---|
| ① 时钟回拨拦截 | 发现回拨 ≤5 ms 就 spin 等待;>5 ms 抛出异常并报警 | 消除 99 % 回拨重复 |
| ② 时钟回拨容忍 | 记录 lastTimestamp,回拨时改用 lastTimestamp+1 并降低序列号位(牺牲吞吐换安全) | 极端场景也绝不重复 |
| ③ workId 自动分配 | 启动时向 ZooKeeper / Redis 申请唯一 workId(持久顺序节点或分布式锁),进程退出即释放 | 避免人工配置重复 |
| ④ 序列号溢出保护 | 序列号 == 4095 时 循环等待下一毫秒,绝不回卷到 0 | 杜绝溢出重复 |
| ⑤ 延长长度 | 把 64 bit 扩展到 128 bit:64 bit 时间 + 48 bit workId + 16 bit 序列号,单节点每毫秒 65 k,可用 8 万年 | 量级无限放大 |
| ⑥ 双缓冲 + 无锁 | 使用 RingBuffer 预填充 ID,CAS 更新序列号,百万级 QPS 无 GC | 性能 & 安全双赢 |
开源增强版示例(百度 UidGenerator、美团 Leaf-Snowflake)都集成以上 ①②③④。
四、最简“防御式”代码片段
java
public class SafeSnowflake {
private long lastTs = -1L;
private long sequence = 0L;
private final long workId;
public synchronized long nextId() {
long curr = System.currentTimeMillis();
if (curr < lastTs) { // 时钟回拨
throw new ClockBackwardsException(curr, lastTs);
}
if (curr == lastTs) {
sequence = (sequence + 1) & 4095;
if (sequence == 0) { // 本毫秒用完,等待
while (curr <= lastTs) curr = System.currentTimeMillis();
}
} else {
sequence = 0;
}
lastTs = curr;
return (curr - EPOCH) << 22 | workId << 12 | sequence;
}
}五、结论 & 面试金句
“雪花算法在 时钟不回拨、workId 不冲突、序列号不溢出 前提下可认为‘全局唯一’;
生产环境必须加 回拨检测 + workId 自动分配 + 溢出自旋,或直接使用 Leaf-Snowflake 等增强实现,才能把重复概率压到‘宇宙规模’。”