sync.mutex在10k+ qps下发号器延迟飙升,因其锁争用需内核态调度;而sync/atomic的atomicaddint64直接编译为cpu xadd指令,无锁且快一个数量级,但须用int64、避免load+store模拟自增,并防范溢出与分布式重复。

为什么 sync.Mutex 不够用,得上 sync/atomic
发号器核心要求是「高并发下不重复、不跳号、低延迟」。用 sync.Mutex 包裹递增逻辑看似安全,但实测在 10k+ QPS 下锁争用严重,Get() 平均延迟飙升到毫秒级。根本原因是每次调用都要进内核态抢锁,而发号本质是单变量原子更新——sync/atomic 的 AtomicAddInt64 直接编译为 CPU 的 XADD 指令,无锁且快一个数量级。
注意:必须用 int64 类型(不是 int),否则在 32 位系统或某些 GC 场景下 AtomicAddInt64 会 panic。
- 初始值建议设为
0或1,避免业务误用负号 - 不要用
atomic.LoadInt64+atomic.StoreInt64模拟自增——这会丢失中间值,必须用AtomicAddInt64(&counter, 1) - 如果需要带时间戳或机器 ID 的雪花算法变体,原子操作只负责序列号段,前缀拼接放外面
如何避免溢出导致的循环回绕
int64 最大值是 9223372036854775807,看起来很大,但若每秒生成 100 万 ID,约 292 年才耗尽。不过生产环境更常见的是误初始化或测试时快速跑满——一旦溢出,AtomicAddInt64 会从正最大值直接跳到负最小值,ID 瞬间变负,下游解析失败。
- 上线前加校验:在
Get()中用if next > math.MaxInt64-1提前 panic,比静默回绕好排查 - 更稳妥的做法是用
uint64+atomic.AddUint64,但 Go 标准库对uint64原子操作支持完整,需确认 Go 版本 ≥ 1.19(旧版部分平台不支持) - 如果业务允许 ID 稀疏,可每生成 100 万个号就主动重启服务,通过部署层面兜底
如何支持多实例部署不重复
单机原子操作解决不了分布式场景。常见错误是让每个实例从 0 开始独立计数——这必然重复。正确做法是把全局唯一因子(如机器 IP、进程 PID、配置中心分配的 workerId)编码进 ID 结构里,只让本地序列号段并发安全。
例如 Twitter Snowflake 风格:41bit 时间戳 + 10bit 机器 ID + 12bit 序列号。其中序列号段用 atomic.Uint64 维护,每次取低 12 位;时间戳和机器 ID 在初始化时算好,全程只读。
- 机器 ID 不能硬编码,应通过环境变量或 etcd 获取,避免镜像打包时写死
- 序列号段用完时(如 12bit 跑满 4096),必须等待下一毫秒再重置,否则时间回拨会导致 ID 重复——这点常被忽略
- 如果不用雪花结构,也可用 Redis 的
INCR做全局号段池,但会引入网络依赖,适合低频场景
为什么别封装成接口,直接暴露函数更安全
看到有人定义 type Generator interface { Next() int64 } 再实现,看似灵活,实际埋雷:接口调用有间接跳转开销,高频调用下比直接函数调用慢 15%–20%;更麻烦的是,接口变量可能被意外复制,导致多个实例共享同一底层原子变量,引发竞态。
推荐扁平化设计:
var globalCounter int64 = 0
func NextID() int64 {
return atomic.AddInt64(&globalCounter, 1)
}
- 全局变量名带
global前缀,明确提示这是跨 goroutine 共享的 - 函数名用
NextID而非Get,语义更精准——这不是查询,是消耗性生成 - 如果项目已用 DI 框架,至少确保该实例在容器中注册为 singleton,且不参与任何 clone 或 copy 流程
真正难的从来不是原子操作本身,而是时间回拨、机器 ID 冲突、溢出策略这些边界条件——它们不出问题时完全隐形,一出就是线上事故。











