不能直接用 time.now().unixnano() 做分布式 id,因其并发下纳秒时间戳易重复、无序、无机器标识,无法满足全局唯一、趋势递增、可排序要求;sonyflake 在容器中 mac 冲突、启动 panic、uint64 符号位及驱动兼容性存陷阱;自研 snowflake 需兜底回拨、自定义 epoch、动态分配 worker id。

为什么不能直接用 time.Now().UnixNano() 做分布式 ID
因为并发下纳秒级时间戳会重复,尤其在单机高 QPS 或多节点同时启动时。time.Now().UnixNano() 本身无序、不可控,也不带机器/进程标识,无法满足「全局唯一 + 趋势递增 + 可排序」三个核心要求。真实场景中,你可能刚上线就遇到数据库主键冲突或分库分表路由错乱——这不是概率问题,是必然发生。
github.com/sony/sonyflake 的实际使用陷阱
它默认用 MAC 地址做 machine ID,但容器环境(Docker/K8s)里常出现多实例获取到相同 MAC,导致 ID 冲突。启动时若网络未就绪,sonyflake.New() 会 panic,而不是返回 error。
- 必须显式传入
sonyflake.Settings{MachineID: func() (uint16, error) { return uint16(os.Getpid() % 1024), nil }},避免依赖网卡 - 初始化要包一层
sync.Once,防止并发调用New()多次触发 panic - 它生成的 ID 是 uint64,但高位 1 位为 sign bit,实际可用只有 63 位;若下游 MySQL 用
BIGINT UNSIGNED存储,需确认驱动是否支持(如go-sql-driver/mysql默认开启parseTime=true时对 uint64 友好)
自研 Snowflake 变种:如何安全处理时钟回拨
原版 Twitter Snowflake 在服务器时间被 NTP 向后校正时会阻塞,而生产环境常见的是向前跳变(如虚拟机休眠后恢复),这时如果只靠 sleep 等待,会导致服务假死。
- 不依赖
time.Sleep等待回拨恢复,改用「记录上一次时间戳 + 允许小范围回拨(如 5ms)内用序列号兜底」 - 把 timestamp 从毫秒改为「自定义纪元(epoch)起的毫秒数」,避免 int64 溢出(例如设 epoch 为
1700000000000,还能撑约 30 年) - worker ID 不硬编码,通过配置中心(如 etcd)分配并监听变更,避免重启后重复
// 示例关键逻辑片段(非完整实现) if ts <h3>Go 标准库 <code>sync/atomic</code> 在 ID 生成器中的误用点</h3> <p>很多人用 <code>atomic.AddUint64(&counter, 1)</code> 实现序列号自增,但没注意 counter 溢出后变成 0 —— 这会直接导致同一毫秒内 ID 重复。Snowflake 类算法中,sequence 字段是有固定位宽的(如 12 位,最大 4095),溢出必须触发等待下一毫秒。</p>
- 别用
atomic直接包裸变量,要用带掩码和条件重置的封装 - 测试时务必覆盖
sequence == sequenceMask的边界情况,否则压测时大概率崩 - 如果用
atomic.CompareAndSwapUint64手动实现,注意失败后要重读当前值再试,否则可能跳过合法值
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











