高并发时 map 崩了?我研究了 GMP 调度后找到了解决方案
高并发时 map 崩了我研究了 GMP 调度后找到了解决方案前言上个月线上出问题了某个服务一到高峰期延迟就飙升偶尔还会出现 data race。查了很久最后发现问题出在 map 上多个 goroutine 并发读写同一个 map虽然加了锁但锁的粒度太大了而且刚好赶上 map 扩容。今天就从 GMP 角度聊聊 map 的坑。一、底层原理1.1 map 怎么扩容和 GMP 有什么关系map 底层是个哈希表数据多了就会扩容。扩容时会做两件事分配新的更大的内存把旧数据重新哈希到新位置graph TD A[Goroutine 1] -- B[写 map] C[Goroutine 2] -- D[写 map] B -- E[触发扩容] E -- F[stop the world] F -- G[所有 G 等待] G -- H[P 利用率骤降]关键点扩容时 map 内部会加全局锁旧数据搬迁需要时间这期间所有访问 map 的 goroutine 都会卡住从 GMP 看就是 P 的本地队列积压调度延迟飙升1.2 map 相关痛点对比问题影响严重程度并发读写不加锁data racepanic 致命全局锁性能瓶颈 高扩容短暂卡顿 高哈希冲突性能下降 中二、快速上手先看反面教材package main import ( fmt sync time ) func main() { m : make(map[int]int) var mu sync.Mutex for i : 0; i 100; i { go func() { for j : 0; j 10000; j { mu.Lock() m[j] j mu.Unlock() } }() } time.Sleep(2 * time.Second) fmt.Println(Done) }这代码虽然不会 panic但锁的粒度是整个 map高并发时性能很差。再看改进版// 用 sync.Map func main() { var m sync.Map for i : 0; i 100; i { go func() { for j : 0; j 10000; j { m.Store(j, j) } }() } time.Sleep(2 * time.Second) fmt.Println(Done) }sync.Map 内部做了优化读多写少时性能很好。三、核心 API / 深水区3.1 map 避坑速查场景推荐方案注意事项单 goroutine普通 map不用锁读多写少sync.Map不要频繁 Store读写均衡分片锁实现复杂一点写多读少分片锁 通道更好的隔离3.2 分片锁原理把 map 分成 N 个小 map每个小 map 一把锁type ShardedMap struct { shards []*shard } type shard struct { mu sync.RWMutex m map[int]int }这样扩容时只影响一个分片不是整个 map。3.3 预分配容量避免扩容知道大概数据量时先分配好// 好 m : make(map[int]int, 100000) // 不好 m : make(map[int]int)四、实战演练写个简单的分片 map对比性能package main import ( fmt sync time ) const ShardCount 32 type Shard struct { mu sync.RWMutex m map[int]int } type ShardedMap struct { shards []*Shard } func NewShardedMap() *ShardedMap { sm : ShardedMap{ shards: make([]*Shard, ShardCount), } for i : range sm.shards { sm.shards[i] Shard{ m: make(map[int]int), } } return sm } func (sm *ShardedMap) getShard(key int) *Shard { return sm.shards[key%ShardCount] } func (sm *ShardedMap) Set(key, val int) { s : sm.getShard(key) s.mu.Lock() s.m[key] val s.mu.Unlock() } func (sm *ShardedMap) Get(key int) (int, bool) { s : sm.getShard(key) s.mu.RLock() val, ok : s.m[key] s.mu.RUnlock() return val, ok } func main() { sm : NewShardedMap() start : time.Now() var wg sync.WaitGroup for i : 0; i 100; i { wg.Add(1) go func() { defer wg.Done() for j : 0; j 100000; j { sm.Set(j, j) sm.Get(j) } }() } wg.Wait() fmt.Printf(耗时: %v\n, time.Since(start)) }和全局锁版本对比快很多。五、避坑指南与最佳实践 **技巧 1预分配 map 容量make(map[T]U, cap)减少扩容次数。⚠️ **警告 1永远不要并发读写普通 map加了锁也不行不加了锁可以但尽量别这么做用 sync.Map 或分片。✅ **推荐读多写少用 sync.Map读写均衡用分片锁。六、综合实战演示完整的高性能 map 方案package main import ( fmt sync sync/atomic time ) type Cache struct { sm *ShardedMap hits int64 misses int64 } func NewCache() *Cache { return Cache{ sm: NewShardedMap(), } } func (c *Cache) Get(key int) (int, bool) { val, ok : c.sm.Get(key) if ok { atomic.AddInt64(c.hits, 1) } else { atomic.AddInt64(c.misses, 1) } return val, ok } func (c *Cache) Set(key, val int) { c.sm.Set(key, val) } func (c *Cache) Stats() (hits, misses int64) { return atomic.LoadInt64(c.hits), atomic.LoadInt64(c.misses) } func main() { cache : NewCache() var wg sync.WaitGroup for i : 0; i 50; i { wg.Add(1) go func() { defer wg.Done() for j : 0; j 100000; j { cache.Set(j, j) } }() } for i : 0; i 50; i { wg.Add(1) go func() { defer wg.Done() for j : 0; j 100000; j { cache.Get(j) } }() } wg.Wait() hits, misses : cache.Stats() fmt.Printf(Hits: %d, Misses: %d\n, hits, misses) }七、总结map 是好东西但高并发场景要小心预分配容量减少扩容不要裸用加保护sync.Map 适合读多写少读写均衡考虑分片锁从 GMP 角度看就是要减少锁粒度减少让 P 等待的时间系统才跑得起来。