Mutex 锁竞争导致 QPS 暴跌?从 GMP 角度看看怎么回事
Mutex 锁竞争导致 QPS 暴跌从 GMP 角度看看怎么回事前言老王为什么本文们的服务 QPS 上不去加了锁反而更慢了 后端工程师小李一脸着急。本文看了看监控发现锁等待时间占比超过 50%。你这是锁竞争太激烈了锁竞争不就是加把锁吗看来得从 GMP 的角度讲起了。今天本文们聊聊 Mutex 锁竞争对性能的影响。一、底层原理1.1 Mutex 竞争对 GMP 的影响当一个 goroutine 尝试获取已经被占的 Mutex 时graph TD A[G1 持有锁] -- B[正在工作] C[G2 请求锁] -- D{锁可用} D --|否| E[G2 阻塞] E -- F[G2 从 P 移除] F -- G[P 调度其他 G] F -- H[G2 进入等待队列] A -- I[G1 释放锁] I -- J[唤醒 G2] J -- K[G2 重新进入 P 队列] K -- L[等待 P 调度]关键影响G2 阻塞时让出 PP 调度其他 GG2 唤醒后还要排队上下文切换比锁操作本身更贵1.2 Mutex 不同模式对比锁模式优点缺点正常模式公平吞吐量低饥饿模式高吞吐不公平读写锁读并发写独占二、快速上手看锁竞争的典型场景package main import ( fmt sync time ) func main() { var mu sync.Mutex counter : 0 var wg sync.WaitGroup start : time.Now() for i : 0; i 1000; i { wg.Add(1) go func() { defer wg.Done() for j : 0; j 10000; j { mu.Lock() counter mu.Unlock() } }() } wg.Wait() fmt.Printf(Mutex: %v, count: %d\n, time.Since(start), counter) }大量协程竞争同一把锁性能惨不忍睹。改进版用分片type ShardedCounter struct { shards [32]struct { mu sync.Mutex value int64 } } func (sc *ShardedCounter) Inc(key int) { idx : key % 32 sc.shards[idx].mu.Lock() sc.shards[idx].value sc.shards[idx].mu.Unlock() }三、核心 API / 深水区3.1 减少锁竞争的技巧速查技巧做法效果缩小临界区只锁必要代码显著读写分离RWMutex读场景好分片锁多把锁显著无锁atomic最好3.2 RWMutex 的正确使用type Cache struct { mu sync.RWMutex data map[string]string } func (c *Cache) Get(key string) string { c.mu.RLock() defer c.mu.RUnlock() return c.data[key] } func (c *Cache) Set(key, val string) { c.mu.Lock() defer c.mu.Unlock() c.data[key] val }高并发读场景RWMutex 比普通 Mutex 快很多。3.3 原子操作替代锁对于简单计数用 atomictype Counter struct { value int64 } func (c *Counter) Inc() { atomic.AddInt64(c.value, 1) } func (c *Counter) Get() int64 { return atomic.LoadInt64(c.value) }四、实战演练对比不同锁方案在高并发下的表现package main import ( fmt sync sync/atomic time ) type AtomicCounter struct { value int64 } func (c *AtomicCounter) Inc() { atomic.AddInt64(c.value, 1) } type MutexCounter struct { mu sync.Mutex value int64 } func (c *MutexCounter) Inc() { c.mu.Lock() c.value c.mu.Unlock() } type ShardedCounter struct { shards [64]shard } type shard struct { mu sync.Mutex value int64 } func (c *ShardedCounter) Inc(key int) { s : c.shards[key%64] s.mu.Lock() s.value s.mu.Unlock() } func main() { n : 1000 iterations : 100000 var wg sync.WaitGroup // atomic ac : AtomicCounter{} start : time.Now() for i : 0; i n; i { wg.Add(1) go func() { defer wg.Done() for j : 0; j iterations; j { ac.Inc() } }() } wg.Wait() fmt.Printf(Atomic: %v\n, time.Since(start)) // Mutex mc : MutexCounter{} start time.Now() for i : 0; i n; i { wg.Add(1) go func() { defer wg.Done() for j : 0; j iterations; j { mc.Inc() } }() } wg.Wait() fmt.Printf(Mutex: %v\n, time.Since(start)) // 分片 sc : ShardedCounter{} start time.Now() for i : 0; i n; i { wg.Add(1) go func() { defer wg.Done() for j : 0; j iterations; j { sc.Inc(j) } }() } wg.Wait() fmt.Printf(分片锁: %v\n, time.Since(start)) }五、避坑指南与最佳实践 **技巧缩小临界区锁的时间越短竞争越少。⚠️ **警告不要用 Mutex 保护只读数据用 RWMutex读操作不阻塞。✅ **推荐能用 atomic 就别上锁atomic 没有上下文切换开销。六、综合实战演示分片缓存减少锁竞争package main import ( fmt sync time ) const shardCount 256 type CacheShard struct { items map[string]int mu sync.RWMutex } type ConcurrentCache struct { shards [shardCount]*CacheShard } func NewCache() *ConcurrentCache { c : ConcurrentCache{} for i : range c.shards { c.shards[i] CacheShard{ items: make(map[string]int), } } return c } func (c *ConcurrentCache) getShard(key string) *CacheShard { hash : 0 for _, b : range key { hash hash*31 int(b) } if hash 0 { hash -hash } return c.shards[hash%shardCount] } func (c *ConcurrentCache) Get(key string) (int, bool) { s : c.getShard(key) s.mu.RLock() val, ok : s.items[key] s.mu.RUnlock() return val, ok } func (c *ConcurrentCache) Set(key string, val int) { s : c.getShard(key) s.mu.Lock() s.items[key] val s.mu.Unlock() } func main() { cache : NewCache() var wg sync.WaitGroup for i : 0; i 100; i { wg.Add(1) go func() { defer wg.Done() for j : 0; j 100000; j { key : fmt.Sprintf(key_%d_%d, j/100, j%100) cache.Set(key, j) cache.Get(key) } }() } wg.Wait() fmt.Println(分片缓存完成) }七、总结Mutex 不是不能用关键是要理解它对 GMP 的影响锁竞争导致 G 阻塞goroutine 让出 P等待锁释放阻塞带来上下文切换P 调度其他 G开销很大上下文切换比锁本身贵这是性能下降的主要原因缩小临界区、分片、atomic 都是好方法减少锁竞争从 GMP 的角度优化锁性能就上去了。