全局 rand.intn() 在高并发下卡住,因所有 goroutine 争用同一 *rand.rand 实例的互斥锁;应为每个 goroutine 创建独立实例,用 crypto/rand 生成唯一种子,或用 sync.pool 复用并重置状态;密码学场景必须用 crypto/rand。

为什么全局 rand.Intn() 在高并发下会卡住
它背后是同一个 *rand.Rand 实例,所有 goroutine 都在抢同一把互斥锁。压测时 QPS 上不去,pprof 显示大量时间花在 runtime.futex 和锁竞争上。哪怕只是读操作(如 Intn),内部也要更新原子计数器,争用不可避免。
每个 goroutine 持有独立 *rand.Rand 实例最简单可靠
*rand.Rand 是约 40 字节的结构体,分配开销极小;关键是种子不能雷同——同一纳秒内多个 goroutine 调 time.Now().UnixNano() 可能拿到相同值。
- 推荐做法:用
crypto/rand.Read()在启动时生成 8 字节种子(仅一次,不进热路径),再传给每个实例 - 次选方案:用
time.Now().UnixNano() ^ int64(id),其中id可用atomic.AddInt64(&counter, 1)生成(比runtime.GoID()更兼容旧版本) - 避免在循环里反复调
rand.New(rand.NewSource(time.Now().UnixNano()))——种子可能重复,且纳秒级时间戳在容器冷启动时易塌缩
用 sync.Pool 复用 *rand.Rand 实例适合中高并发
避免频繁分配,又不牺牲并发安全。但注意:Pool 中对象可能被 GC 回收,所以每次 Get() 后必须重置状态。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- Go r.Seed(time.Now().UnixNano())
- Go 1.20+:不能直接改
r.src(已封装),建议 Pool 存的是“已设好种子”的实例,New函数里用crypto/rand生成新种子并缓存复用 - 别在
sync.Pool.New里写rand.New(rand.NewSource(time.Now().UnixNano()))——种子会过期,且池中多个实例可能撞种子
什么时候该换 crypto/rand?不是因为“更快”,而是“不能不用”
当你要生成 token、session ID、salt、UUID v4 或任何用于密码学上下文的值时,math/rand 再快也不能用。它的输出可预测、分布偏差不可控、不抗重放。
-
crypto/rand.Read(buf)在 Linux 上批读/dev/urandom,实测 10K goroutine 并发下比加锁的math/rand还快 - 别自己拼
rand.Int63() + fmt.Sprintf造 token——长度不定、分布不均、不安全 - 需要字符串?直接用
uuid.NewRandom(),它底层就是crypto/rand,且做了模削减和字节填充
真正容易被忽略的点是:安全边界不是“性能够不够”,而是“是否暴露攻击面”。一旦你把 math/rand 用在鉴权流程里,修复成本远高于初期多写一行 crypto/rand.Read()。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










