用atomic.Pointer配合写时复制可彻底消除读锁,适用于读多写少、数据不可变、整体替换场景,P99延迟从4ms降至0.6ms,但需权衡写端复制开销与GC压力。

热路径上锁竞争直接卡死吞吐量,不是“优化锁”,而是“绕开锁”或“让锁不热”。关键看操作类型:纯计数用 atomic,读多写少用 sync.RWMutex,写密集且需聚合用分片,状态流转复杂就走 channel 串行化。
热路径计数器别用 mutex,改用 atomic.Int64
所有 goroutine 都往一个 int64 上加 1?sync.Mutex 会把整个 CPU 核心拖进调度队列。实测 32 核机器上,mutex 计数器吞吐常低于 100 万 ops/s;而 atomic.AddInt64 轻松破千万。
- 只适用于单变量、无条件更新(如
counter++、开关标志位) - 禁止混用:不能一边
atomic.StoreInt64(&x, 1),一边x = 2,会触发 data race - 别在热路径反复
atomic.LoadInt64读取再判断——如果逻辑是“仅当为 0 才设为 1”,必须用atomic.CompareAndSwapInt64
读多写少的缓存/配置访问,必须用 RWMutex 且只读不写
sync.RWMutex 在读占比超 70% 时才有价值。但常见错误是:在 RLock() 后调用外部函数,而该函数内部偷偷改了共享字段,导致 panic 或死锁。
- 读操作里禁止调用任何可能写共享数据的方法(比如 lazy-init 函数未加防护)
-
RLock()和RUnlock()必须成对,漏掉一个RUnlock(),后续所有Lock()就永久阻塞 - 别在
defer里无条件RUnlock():如果RLock()因 context 取消失败,不该解锁 - 写操作频率一旦超过 10%,RWMutex 的锁状态管理开销反而比 Mutex 更重
高频写入的 map 或结构体,必须分片(Sharding)
一把锁保护整个 map[string]int 是最典型的热路径瓶颈。哪怕只有 5% 的 key 被高频更新,其余 95% 的读也得排队。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- 用
hash(key) % N映射到固定大小的锁数组,N 推荐 32–256(通常取 CPU 核数的 2–4 倍) - 每个分片锁只保护自己桶内的子 map 或字段,无关 key 完全不冲突
- 避免用
map[int]*sync.Mutex存锁——map 本身非并发安全,扩容时会 panic - 分片数过大会增加内存和 cache line false sharing 风险;过小则竞争照旧
无法拆解的复合写逻辑,用 channel 把并发转为串行
有些操作天生不能并行:比如聚合指标、写 ring buffer、更新带依赖关系的状态机。硬加锁只会堆满 goroutine 队列,不如主动收口。
- 定义一个
chan *writeReq,所有写请求发进去 - 启动单个 goroutine 消费 channel,顺序执行写逻辑、更新共享状态
- 读操作仍可无锁或用 RWMutex,不受写 goroutine 影响
- 注意 channel 缓冲区大小:太小会导致发送方阻塞;太大则内存占用高,且延迟不可控
真正难的不是选哪种方案,而是确认“哪段是热路径”——不跑 go tool pprof 看 runtime.futex 调用占比,光靠猜只会优化错地方。

















