sync.RWMutex 是读写分离缓存的首选,因其提供原生读共享、写独占语义,高读低写场景下允许多读并发、写时强一致;但不适用于高并发写,且需防写锁饥饿、数据污染与功能缺失问题。

为什么 sync.RWMutex 是读写分离缓存的首选
因为 Go 原生不提供「读共享、写独占」的原子语义,sync.RWMutex 是标准库中唯一轻量且线程安全的实现。它比全用 sync.Mutex 提升明显——高读低写场景下,多个 goroutine 可同时读,无需排队;而写操作会阻塞所有新读和新写,保证数据一致性。
注意:它不是为「大量并发写」设计的。一旦写操作变多,读也会被频繁阻塞,此时该考虑分片(sharding)或换用更复杂的结构(如 singleflight + sync.Map 组合),而不是硬扛。
怎么用 sync.RWMutex 包裹一个简单缓存结构
核心是把锁粒度控制在「整个缓存实例」级别,而不是每个 key 单独加锁(除非你做分片)。典型结构是一个 map + 读写锁:
type Cache struct {
mu sync.RWMutex
data map[string]interface{}
}
func (c *Cache) Get(key string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
v, ok := c.data[key]
return v, ok
}
func (c *Cache) Set(key string, value interface{}) {
c.mu.Lock()
defer c.mu.Unlock()
if c.data == nil {
c.data = make(map[string]interface{})
}
c.data[key] = value
}
-
Get用Rlock/RUnlock,允许多个 goroutine 并发读 -
Set用Lock/Unlock,写时彻底排他 - 必须在
Set中检查c.data == nil,否则首次写 panic:nil map assignment - 别在
Get里做耗时操作(比如反序列化、网络调用),锁持有时间越短越好
常见踩坑:RLock 后不能安全修改底层数据
很多人以为「只读锁保护了 map,那读出来的东西就能随便改」,这是错的。比如:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
v, _ := c.Get("user")
u := v.(*User)
u.Name = "hacked" // ⚠️ 这会污染原始缓存值!
原因:Go 的 map 存的是指针或值副本,但结构体字段赋值直接作用于原内存。如果缓存里存的是指针(如 *User),外部修改会穿透到缓存内部。
- 解决方案一:缓存只存不可变值(
string、int、struct{...}值类型) - 解决方案二:
Get返回深拷贝(用encoding/gob或copier库) - 解决方案三:业务层约定「只读取,不修改返回值」,靠文档+Code Review 约束
什么时候不该用 sync.RWMutex 直接包 map
当你的缓存需要以下任意能力时,sync.RWMutex + map 就不够用了:
- 自动过期(TTL)→ 需要定时清理或惰性删除,得引入
time.Timer或第三方库如github.com/patrickmn/go-cache - 内存限制与 LRU 驱逐 →
map不支持容量控制,得换container/list+map手写,或用github.com/hashicorp/golang-lru - 写后通知(如发布更新事件)→ 锁本身不提供 hook,需额外 channel 或回调机制
- 跨进程共享 →
sync.RWMutex是进程内同步原语,这时候该上 Redis 或本地 mmap 文件
最常被忽略的一点:sync.RWMutex 的写锁饥饿问题——持续读请求会让写一直等不到机会。如果你的业务有强实时写需求(比如配置热更新),得加写优先逻辑或降级为普通 Mutex。

















