不能直接用 time.Now().UnixNano() 做分布式 ID,因其在高并发、多节点、时钟回拨等场景下必然重复,且无机器标识与序列号,无法满足全局唯一、趋势递增、可排序等核心要求。

为什么不能直接用 time.Now().UnixNano() 当分布式ID
因为毫秒级时间戳在多节点下极易重复,哪怕加进程ID或机器IP,只要并发高、时钟回拨或虚拟机漂移,1672531200000000 这种纯时间ID马上撞车。真实微服务里,你看到的 duplicate key violation 或 unique constraint failed 错误,八成是这里出的问题。
真正能用的方案得满足:全局唯一、趋势递增、无中心依赖、低延迟。Snowflake 是底线,但 Go 原生没内置,得自己小心实现。
- 别用
math/rand生成序列号——它不是并发安全的,多个 goroutine 同时调用会产出相同sequence - 机器 ID 别硬编码进代码,得从配置或注册中心(如 etcd / Consul)动态获取,否则部署新实例就冲突
- 位分配必须对齐:默认 Snowflake 是 41b 时间戳 + 10b 机器 ID + 12b 序列号 = 63b,超了
int64最大值就溢出变负数
github.com/bwmarrin/snowflake 能直接用吗
能,但要注意它默认用本机 MAC 地址生成 node ID,Docker 容器里所有实例可能拿到同一个 MAC,导致 ID 冲突。更糟的是,它用 sync.Mutex 保护 sequence 计数器,QPS 上万时会成为瓶颈。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 初始化时强制指定
node:用环境变量NODE_ID或从 K8s Downward API 拿pod.name的哈希后取模 - 把
node改成 uint16 类型,避免和时间戳高位冲突;确认sf.Node() == your_node_id再发号 - 别在 HTTP handler 里每次都
sf.Generate()——提前批量预生成 100~1000 个 ID 放 channel 里,按需取,减少锁争用
自研 Snowflake 时怎么防时钟回拨
回拨不是“异常”,而是常态:容器重启、NTP 校时、云主机休眠都可能让系统时间倒退。一旦回拨,原生 Snowflake 直接 panic 或阻塞,你的服务就卡死。
稳妥做法是容忍小范围回拨(比如 5ms),并引入补偿机制:
- 记录上次发号时间
lastTimestamp,每次生成前比对:if timestamp 则进入回拨处理分支 - 若回拨 ≤ 5ms,sleep 等待到
lastTimestamp + 1;超过则抛错或降级用 UUID(注意:UUID 不递增,会影响 MySQL B+Tree 性能) - 务必用
time.Now().UnixMilli()(Go 1.17+)而非UnixNano(),毫秒精度已够用,且减少低位碰撞概率
要不要考虑 leaf-segment 或 Redis INCR 方案
Leaf-segment(美团开源)适合 ID 需求极高的场景(如每秒百万级),但它强依赖 MySQL,单点故障风险高;Redis INCR 更简单,但网络延迟不可控,且 Redis 故障时整个发号服务瘫痪。
Go 微服务中,除非你已有成熟分库分表+高可用 Redis 集群,否则别为了“看起来先进”而引入新故障点。一个带回拨保护、node ID 动态注入、sequence 无锁优化的 Snowflake 实现,已经覆盖 95% 场景。
真正容易被忽略的是:不同服务用同一套 ID 生成器时,必须确保它们的 epoch(起始时间)完全一致,哪怕差 1ms,最终生成的 ID 也会错位——这个值最好写死在 config.yaml 里,而不是用 time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli() 动态算。


















