核心问题是时间回拨、机器ID冲突、序列号溢出未兜底:时钟回退致序列复用;多进程共用默认workerId撞车;序列超4095未等待下一毫秒;位移量错位(如timestamp应左移22位、workerId移12位)导致ID错乱。

为什么直接手写 snowflake 容易生成重复 ID
核心问题不是算法逻辑难,而是时间回拨、机器 ID 冲突、序列号溢出这三处没兜住。比如本地时钟被 NTP 校准导致 System.currentTimeMillis() 回退 5ms,同一毫秒内序列号没重置,就可能复用旧值;又或者测试环境多进程共用默认 workerId=1,ID 直接撞车。
- 必须自己管理
workerId分配:用 ZooKeeper 临时节点、Redis INCR 或配置中心下发,别硬编码 - 时间回拨要拦截:检测到回拨超过 5ms 就阻塞等待,或抛异常让上游重试(不能静默跳过)
- 序列号用
AtomicInteger,上限设为4095(12bit),到 4095 后必须等下一毫秒,否则越界归零
nextId() 方法里哪些位运算容易写反
雪花算法的 64bit 拆分是固定套路:1bit(符号)+41bit(时间戳)+10bit(workerId)+12bit(sequence)。错一位,整个 ID 就乱序甚至变负数——比如把 timestamp 左移 22 位(应为 22?不,是 22 = 10+12),实际得移 22 位;workerId 要左移 12 位,不是 10 位。
- 时间戳截取用
System.currentTimeMillis() - EPOCH,EPOCH必须是自定义起始毫秒(如1717027200000L),别用new Date().getTime() - 拼装时用
|(按位或),不是+;各段必须先左移到对应位置再或,顺序无所谓,但位数不能错 - 示例关键行:
(timestamp —— 这里 <code>22是10+12,12是序列位宽
单机多实例部署时 workerId 怎么安全分配
同一台机器跑两个 Spring Boot 应用,如果都用 System.getProperty("worker.id") 且没配参数,workerId 默认都是 0,ID 必然重复。靠端口号、PID、MAC 地址自动推算也不可靠——端口可能复用,PID 会回收,MAC 在容器里常为空。
- 最稳的是启动时从外部注入:
java -Dworker.id=5 -jar app.jar,配合运维脚本统一管理 - 次选 Redis 原子分配:用
INCR worker_id_seq获取唯一值,再存入本地缓存,避免每次调用都打 Redis - 绝对不要用
InetAddress.getLocalHost().getHostAddress()算哈希——Docker 里返回127.0.0.1,全集群都一样
高并发下 sequence 溢出的实际表现和应对
每毫秒最多 4096 个 ID。如果 QPS 稳定在 5000,那每毫秒必溢出一次,sequence 归零后没等新毫秒就继续生成,结果就是时间戳相同、workerId 相同、sequence 从 0 开始重复——ID 重复率瞬间拉满。
立即学习“Java免费学习笔记(深入)”;
- 监控要盯
waitMs:每次因 sequence 满而等待的毫秒数,持续 >0 就说明扛不住了 - 别用
Thread.sleep()等下一毫秒——精度差还阻塞线程;改用自旋 +System.currentTimeMillis()对比 - 真实场景建议预留 buffer:比如压测发现峰值 4500 QPS,那就至少部署 2 个
workerId实例分摊压力
事情说清了就结束。真正上线前,拿 Set<long></long> 跑一小时压测看有没有重复,比读十遍源码都管用。










