sync.map 不适合共享缓存,因其缺乏 ttl 支持、无一致性遍历、无 len 方法、load/delete 非原子,且高并发下性能反不如 map+sync.rwmutex;推荐用后者配合惰性过期检查与定期批量清理。

别用 sync.Map 做共享缓存,尤其带 TTL 或需遍历的场景——它不是并发安全 map 的通用解,而是特定负载下的权衡产物。
为什么 sync.Map 不适合共享缓存
共享缓存通常要求:按需过期、批量清理、Get 时校验并驱逐、支持精确长度或全量遍历。而 sync.Map 在这些点上全部缺失:
-
Range()不提供一致性快照,遍历时可能漏掉刚Store()进dirty的新 key,也可能重复看到已Delete()但尚未同步到read的旧值 - 没有
Len()方法,无法快速判断缓存规模;Range()计数是 O(n) 且非原子,高并发下结果不可靠 -
Load()+ 手动判断过期 +Delete()无法原子执行,中间存在竞态窗口,导致脏读(返回已过期值) - 频繁
Delete()会触发内部桶分裂和dirty→read同步开销,实测比加锁 map 更慢
真正可控的替代方案:map + sync.RWMutex
90% 的共享缓存需求,直接用原生 map 配 sync.RWMutex 更清晰、更易调试、性能也不差:
- 读操作用
RUnlock()/RLock(),写操作用Lock()/Unlock(),锁粒度明确 - 可在一次写锁内完成“查+判过期+删+重插”整套逻辑,避免竞态
- 支持
len(m)、for range m、delete(m, key)等原生语义,代码直白 - 值类型无需
interface{}装箱,小整数、结构体等零逃逸,GC 压力低
带 TTL 缓存的关键实现细节
过期处理不能依赖每个 key 单独 timer,也不能在 Get 里升级锁删 key —— 正确做法是惰性检查 + 定期扫描:
-
Get(key)时只拿RLock,读出 value 后立刻检查过期时间,若过期则return nil, false(不删) - 另起 goroutine 每秒调用
cleanExpired():先Lock(),遍历 map,批量删除过期项,再Unlock() - 禁止为每个 key 启 goroutine 或
time.AfterFunc,否则 10k key 就 10k goroutine - 若缓存规模大,可考虑分片
map[int]*shard+ 分片锁,降低单锁竞争
什么时候才考虑 sync.Map
仅当满足全部以下条件时,sync.Map 才值得尝试:
- 读操作占比 >90%,写极少(如服务启动后只增不改的配置映射)
- key 生命周期极长,几乎不
Delete(),也不需要遍历或批量操作 - 不依赖
len(),不关心Range()是否覆盖所有当前 key - value 是简单类型(
string、int64),能接受interface{}拆装开销
哪怕只有一条不满足,就老实用 map + sync.RWMutex。它的“手动加锁”不是缺陷,而是对并发意图的显式表达——这点恰恰是 sync.Map 最容易被忽略的代价。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











