如何用雪花算法生成分布式唯一 ID
结论:雪花算法(Snowflake)用 64 位 long 把「毫秒时间戳 + 机器 ID + 序列号」拼成一个本地生成的、全局唯一的、趋势递增的 ID,单机每秒可产出 409.6 万个,不依赖网络和中心服务,是目前分布式 ID 的主流方案。
雪花算法的 64 位是怎么分配的?
结论:1 位符号位 + 41 位时间戳 + 10 位机器 ID + 12 位序列号,共 64 位,正好塞进一个 Java long。
- 1 位符号位:恒为 0,保证 ID 是正数。
- 41 位毫秒时间戳:存的是「当前毫秒 − 自定义起始纪元」。2^41 毫秒 ≈ 69.7 年,纪元选 2020-01-01 00:00:00 UTC(1577808000000L)可用到 2089 年。
- 10 位机器 ID:拆成 5 位数据中心 ID + 5 位工作机器 ID,最多 32 × 32 = 1024 个节点。
- 12 位序列号:同一毫秒内自增,范围 0~4095,也就是单节点每毫秒最多 4096 个 ID,折合每秒 409.6 万个。
雪花算法怎么实现?
结论:核心是 nextId() 方法里的三段判断——是否时钟回拨、是否同一毫秒、序列号是否溢出。
public class SnowflakeIdGenerator {
private static final long EPOCH = 1577808000000L; // 2020-01-01 UTC
private static final long SEQUENCE_MASK = ~(-1L << 12); // 4095
private static final long TIMESTAMP_SHIFT = 22L; // 12+5+5
private static final long DATACENTER_SHIFT = 17L; // 12+5
private static final long WORKER_SHIFT = 12L;
private final long workerId, datacenterId;
private long sequence = 0L, lastTimestamp = -1L;
public synchronized long nextId() {
long ts = System.currentTimeMillis();
if (ts < lastTimestamp) { // 时钟回拨
long offset = lastTimestamp - ts;
if (offset <= 5) ts = waitUntil(lastTimestamp); // 5ms 内自旋等待
else throw new IllegalStateException("时钟回拨 " + offset + "ms");
}
if (ts == lastTimestamp) {
sequence = (sequence + 1) & SEQUENCE_MASK;
if (sequence == 0) ts = waitUntil(lastTimestamp + 1); // 本毫秒用尽
} else {
sequence = 0L;
}
lastTimestamp = ts;
return ((ts - EPOCH) << TIMESTAMP_SHIFT)
| (datacenterId << DATACENTER_SHIFT)
| (workerId << WORKER_SHIFT)
| sequence;
}
}
nextId() 必须加 synchronized 或改用 AtomicLong,否则并发下序列号会算错。
时钟回拨怎么办?
结论:小回拨等待、大回拨报错是最简单也最稳的策略,回拨超过 5ms 直接抛异常触发告警,不要硬扛。
时钟回拨的常见原因:NTP 校时、虚拟机迁移、容器宿主机时间被手动修改。三种主流处理方式:
- 等待:回拨 ≤ 5ms 时自旋到
lastTimestamp再生成,业务无感知。 - 备用 workerId:美团 Leaf 的做法,回拨时切换到另一个机器 ID,避免生成重复 ID。
- 拒绝服务:回拨过大直接抛异常,让上层重试或降级。
生产环境建议所有节点开启 NTP 同步,并监控回拨次数。
10 位机器 ID 怎么分配不重复?
结论:容器化环境不要用「IP 后 10 位」或 hostname 哈希,优先用 Kubernetes StatefulSet 的序号(0~1023),非 K8s 环境用 Redis 或 ZooKeeper 注册。
可选方案对比:
- Redis INCR:启动时
INCR snowflake:worker取值并对 1024 取模,简单但有回收问题。 - ZooKeeper 顺序节点:Leaf 用的方案,临时节点 + 顺序号,宕机自动释放。
- MySQL 自增表:节点启动时 INSERT 一次拿自增 ID,配合心跳表清理。
- K8s StatefulSet:直接用 Pod 名
app-0、app-1的数字部分,最省事。
雪花算法和 UUID、数据库自增 ID 有什么区别?
结论:UUID 无序导致 InnoDB 聚簇索引频繁页分裂,数据库自增 ID 有单点和分库分表难题,雪花算法兼顾有序性和去中心化。
| 方案 | 长度 | 趋势递增 | 依赖 |
|---|---|---|---|
| UUID v4 | 128 位(36 字符) | 否 | 无 |
| MySQL 自增 | 64 位 | 是 | 数据库单点 |
| Redis INCR | 64 位 | 是 | Redis |
| 雪花算法 | 64 位 | 近似递增 | 仅需分配 workerId |
雪花 ID 在同一节点内严格递增,跨节点只保证按毫秒趋势递增,这对 MySQL 主键索引已经足够友好。
返回给前端要注意什么?
结论:雪花 ID 是 64 位,超过 JavaScript Number.MAX_SAFE_INTEGER(9007199254740991),必须序列化成字符串返回。
Jackson 里加 @JsonSerialize(using = ToStringSerializer.class) 即可,否则前端拿到的 ID 末几位会被截断成 0,出现「两条数据 ID 一样」的诡异 bug。
总结一下:雪花算法的 64 位结构决定了它的上限是单节点 409.6 万 ID/秒、1024 个节点、69.7 年,落地时要解决三件事——nextId() 加锁、时钟回拨策略、workerId 分配,最后别忘了把 ID 以字符串形式吐给前端。
原文链接:https://www.gj0.com/thread-303.html
转载请注明出处并保留本声明;内容仅代表作者观点,与本站立场无关。若本文涉嫌侵权,请联系本站处理。