不能直接用 time.Now().UnixNano() 做ID,因其在高并发和跨机器场景下不唯一且不单调;推荐使用 Snowflake(如 sony/sonyflake),需合理设置 machineID(如 K8s 中用 hostname 哈希取低10位),并配合预分配优化性能。

为什么不能直接用 time.Now().UnixNano() 做ID
时间戳本身不唯一,高并发下纳秒级也可能重复;跨机器部署时系统时钟不同步会进一步放大冲突风险。哪怕加随机数或PID,也难保证分布式场景下的严格唯一性和单调递增性——尤其在订单、支付、日志追踪等强一致性场景下,重复ID会导致数据错乱或幂等失效。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 避免拼接
time.UnixNano()+rand.Intn(1000)这类“看起来够用”的方案 - 不要依赖单机
atomic.Int64自增——它无法跨进程/跨实例共享状态 - 若服务已上K8s,别假设所有Pod共享同一台宿主机时钟,NTP漂移真实存在
推荐用 snowflake,但要注意 machineID 怎么设
Twitter Snowflake 是最成熟的选择:41位时间戳 + 10位机器ID + 12位序列号,支持每毫秒生成4096个ID,且天然有序。Go生态里 sony/sonyflake 和 bwmarrin/snowflake 都可用,但machineID设置不当会引发ID碰撞。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 用容器环境时,
machineID别硬编码为固定数字(如1),否则所有副本生成相同ID段 - K8s中可读取
/etc/hostname或POD_NAME环境变量做哈希,再取低10位:int(hash(hostname) & 0x3FF) - 若服务部署在VM而非容器,可用MAC地址前两字节异或后取模,比用IP更稳定(IP可能动态变更)
-
sony/sonyflake默认用os.Getpid()当machineID,但在容器里多个goroutine共用同一PID,必须显式覆盖
本地缓存+预分配能扛住突发流量,但别忘了 sync.Pool 不是万能的
单纯调用 Snowflake.NextID() 在QPS超5k时可能因锁竞争成为瓶颈。常见优化是预取一批ID放入本地池,减少对全局state的争抢。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 用
sync.Pool缓存ID切片,每次取100个,用完再批量申请——注意Pool对象不保证存活,不能当长期存储 - 更稳的做法是启动时预生成1w个ID到内存队列(
chan int64),用select { case id := 非阻塞获取 - 别把预分配数量设得过大(比如10w),OOM风险随实例数线性增长;建议按单实例峰值QPS × 100ms窗口估算
- 如果用了gRPC流式接口或长连接场景,需确保每个goroutine从独立ID源取值,避免不同请求混用同一缓存池导致ID跳跃不连续
测试阶段最容易漏掉的三个边界
本地跑通不等于线上安全。以下问题常在压测或灰度时才暴露:
- 时钟回拨:容器重启或宿主机NTP校正可能导致
time.Now().UnixMilli()下降,snowflake库若没启用等待策略(如sonyflake.WithTimeFunc自定义阻塞逻辑),会panic或返回重复ID - 机器ID冲突:两个不同集群的Pod恰好算出相同
machineID(比如都取了hostname哈希的低10位),需要加集群标识前缀或改用etcd注册中心动态分配 - 序列号溢出:每毫秒4096个ID看似充裕,但如果某次GC停顿长达2ms,下一毫秒初序列号重置为0,而前一毫秒末尾ID和当前开头ID可能被业务误判为“时间倒序”
真正麻烦的不是ID重复本身,而是重复发生时系统没有明确报错路径——比如日志里只打"insert failed: duplicate key",却没记录当时生成的ID和上下文。上线前务必在ID生成函数里埋点,至少记下 machineID、当前时间戳、序列号值。


















