不能直接用time.Now().UnixNano()作ID,因其在高并发、容器化、时钟回拨等场景下极易重复;微服务中应选用sonyflake等snowflake变种,显式配置StartTime和MachineIDFunc,并妥善处理时钟回拨与序列溢出。

直接用 time.Now().UnixNano() 或数据库自增主键,在微服务里撑不过 5 分钟就会报 duplicate key error——这不是配置问题,是架构层面的错配。
为什么不能用 time.Now().UnixNano() 做 ID
纳秒级时间戳在 Go 并发场景下根本不可靠:goroutine 调度、CPU 时钟抖动、容器重启后时钟回拨,都会导致重复。实测单机压测 2k QPS 就开始撞号,尤其在 sync/atomic 未对齐或没加锁的自增逻辑里。
-
time.Now().UnixNano()返回的是系统时钟快照,不是单调时钟;clock_gettime(CLOCK_MONOTONIC)才单调,但不跨进程唯一 - 容器环境(如 K8s)中多个 Pod 共享宿主机时钟源,同一纳秒内多个实例同时调用,ID 必然重复
- JavaScript 消费端会因
Number.MAX_SAFE_INTEGER(253−1)截断高 64 位整数,传字符串又和 RPC 的int64字段定义冲突
sonyflake 是目前最稳妥的轻量选择
它强制你显式处理节点 ID 和起始时间,避免隐式依赖(比如 bwmarrin/snowflake 默认用 PID,Pod 重启就撞号)。
- 必须设置
StartTime,否则高位全 0,MySQL B-tree 索引效率下降 20%+;推荐设为部署前 1 小时:time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) - 节点 ID 推荐用
os.Hostname()+ 启动时间哈希生成,或从环境变量读HOSTNAME和POD_NAME拼接再哈希 - 时钟回拨默认 panic,生产环境需包一层
recover,并 fallback 到本地缓存段(如预取 100 个 ID)
自己手写 Snowflake 时最容易漏掉的三件事
结构看着简单,但线程安全和边界判断一漏就崩。
立即学习“go语言免费学习笔记(深入)”;
- 序列号溢出必须重置:当
s.sequence == maxSequence时,不能继续自增,得等下一毫秒,否则 ID 乱序甚至倒流 - 时间戳比较要用
time.Now().UnixMilli()(Go 1.17+),别手动除 1e6 —— 浮点误差会导致毫秒截断错误 -
nodeID必须校验范围:if nodeID 1023,否则位移后高位污染时间戳段
真正难的不是算出那 64 位数字,而是让每个节点在任意时刻、任意调度节奏下,都交出一个不重复、不倒退、能被下游安全消费的整数——这要求你对 Go 的并发模型、系统时钟行为、以及上下游数据契约有明确判断,而不是抄一段代码就完事。


















