直接用 goroutine 更新缓存会丢数据,因「读-改-写」操作非原子;sync.Map 不解决该问题;推荐 per-key Mutex + double-check 或 singleflight.Group 配合手动缓存。

为什么直接用 goroutine 更新缓存会丢数据
多个 goroutine 同时调用 cache.Set(key, value) 更新同一 key,若底层是 map + mutex,看似安全;但若更新逻辑含「读-改-写」(如计数器自增、结构体字段合并),就极易因竞态丢失更新。典型表现是:并发 100 次 inc("counter"),最终值却小于 100。
用 sync.Map 替代普通 map 不解决根本问题
sync.Map 只保证单个操作(Load/Store)原子,不支持原子的「读-改-写」组合。例如:
val, ok := cache.Load("counter")
if ok {
cache.Store("counter", val.(int)+1) // 中间可能被其他 goroutine 覆盖
}
这段代码仍存在竞态。真正需要的是对某个 key 的**独占更新权**,而非仅线程安全的容器。
推荐方案:per-key Mutex + double-check
为每个缓存 key 维护一个独立的 *sync.Mutex(用 sync.Pool 复用避免频繁分配),配合双检锁模式,确保同一时刻只有一个 goroutine 执行更新逻辑:
立即学习“go语言免费学习笔记(深入)”;
- 先尝试
cache.Load(key),命中则直接返回 - 未命中则获取该 key 对应的 mutex,再次检查是否已由其他 goroutine 写入(防止重复加载)
- 仅当二次检查仍为空时,才执行耗时更新(DB 查询、HTTP 请求等),再
cache.Store()
注意:mutex 实例不能直接存 map(会导致 GC 压力),应使用 map[string]*sync.Mutex + sync.Once 或固定大小分片锁(如 64 个 sync.RWMutex 按 key hash 分配)。
更稳妥的选择:使用 singleflight.Group
Go 官方 golang.org/x/sync/singleflight 是专为此类场景设计的——对相同 key 的并发请求,只让第一个 goroutine 执行函数,其余等待其结果并共享返回值。它天然解决「缓存击穿」和「重复更新」问题:
var group singleflight.Group
func getWithCache(key string) (interface{}, error) {
v, err, _ := group.Do(key, func() (interface{}, error) {
// 这里是真实加载逻辑,只执行一次
return fetchFromDB(key)
})
return v, err
}
注意:singleflight.Group 不自动缓存结果,需配合外部缓存(如 sync.Map)在 Do 回调里手动 Store;否则每次缓存失效后仍会触发一次 group.Do 阻塞等待。
实际部署时,最易被忽略的是「更新失败后的错误处理」:singleflight 默认不缓存 error,若 fetchFromDB 返回 error,下次同 key 请求仍会重试——这可能导致雪崩。应在回调中显式判断 error 并决定是否写入空/过期值到缓存。










