高并发服务里锁竞争拖慢接口的排查与优化思路

高并发服务里锁竞争拖慢接口的排查与优化思路

1024KB1024KB 全栈开发 2026-07-22 17:56:48 浏览 67107 字数 511

做后端开发的同学大概率都遇到过这种情况:单机 QPS 没到瓶颈,CPU 利用率也不高,可接口耗时就是忽高忽低。翻完监控常常发现,问题出在锁竞争上——多个协程或线程争抢同一把锁,谁都跑不快。

从一次订单超时说起

之前负责的交易服务,在下单高峰时 P99 延迟从 30ms 涨到 800ms。pprof 的 mutex profile 显示,有一把保护本地缓存的互斥锁被大量 Goroutine 等待。简化逻辑如下:

var cacheMu sync.Mutex
var localCache = map[string]*Item{}

func GetItem(key string) *Item {
    cacheMu.Lock()
    defer cacheMu.Unlock()
    if it, ok := localCache[key]; ok {
        return it
    }
    it := loadFromDB(key)
    localCache[key] = it
    return it
}

这里读多写少,却用了排他锁,所有读请求被串行化,锁竞争自然严重。

读多写少用读写锁分场景

最常见的优化是把 Mutex 换成 RWMutex,读操作不互斥:

var cacheMu sync.RWMutex

func GetItem(key string) *Item {
    cacheMu.RLock()
    it, ok := localCache[key]
    cacheMu.RUnlock()
    if ok {
        return it
    }
    cacheMu.Lock()
    defer cacheMu.Unlock()
    if it, ok = localCache[key]; ok {
        return it
    }
    it = loadFromDB(key)
    localCache[key] = it
    return it
}

改完后读路径的锁竞争基本消失,P99 回到 40ms 内。注意双检锁里第二次检查不能省,否则可能重复加载。

锁粒度太粗就拆细

有时一把大锁保护整个 map,可以按 key 分桶。下面用 32 个分片降低冲突:

const shards = 32
var buckets [shards]struct {
    sync.RWMutex
    m map[string]*Item
}

取模定位分片,只锁对应桶。压测下冲突次数降了一个数量级。

无锁方案更适合高频计数

如果是计数器类需求,atomic 比锁轻得多:

var reqCount int64
atomic.AddInt64(&reqCount, 1)

对于简单数值,无锁原子操作避免调度开销,也谈不上锁竞争。

几个日常排查习惯

  • go test -race 及早暴露数据竞争
  • 生产环境开 net/http/pprof,看 mutex 与 block profile
  • 压测时观察 Goroutine 堆积和锁等待时间

把热点锁换成合适结构,往往比加机器见效更快。

分享到:

评论

加载中...
我
还没有评论,快来抢沙发吧~