golang-lru 的 rwmutex 不是万能解法,因其无法保证 containsoradd 等复合操作的原子性,需改用 lock();sync.map 无访问序维护能力,不适用于需淘汰策略的场景;arc/2q 中队列长度读取也需 lock() 防状态错乱;defer unlock 无法覆盖 panic 或早 return 场景,锁释放须与业务语义对齐。

golang-lru 的 RWMutex 为什么不是万能解法
读写锁在缓存场景里确实高效,但 sync.RWMutex 并不能覆盖所有并发路径。比如 ContainsOrAdd 这类复合操作,它先查再写,中间存在竞态窗口——即使用了 RLock(),查完释放锁、加锁写入前,其他 goroutine 可能已插入同 key。所以 golang-lru 对这类方法统一用 Lock(),牺牲部分读性能换原子性。
容易踩的坑:
- 误以为
RWMutex能自动保证复合逻辑安全,结果出现重复写或漏更新 - 在
onEvictedCB回调里做耗时操作(如 HTTP 请求),阻塞写锁,拖慢整个缓存吞吐 - 没用
defer c.lock.Unlock(),panic 时锁未释放,导致后续所有操作永久阻塞
sync.Map 和 golang-lru 的适用边界在哪
sync.Map 是 Go 标准库为“键集合不相交”或“读多写少”设计的并发 map,但它不是 map 的替代品,更不适合作为 LRU 使用:它不维护访问顺序,无法实现淘汰策略;LoadOrStore 返回 bool 表示是否新存入,但你没法知道这个 key 是第几次被访问。
而 golang-lru 的核心价值在于「有序淘汰」+「并发安全」的组合。它用 simplelru.LRU 管理链表顺序,外层用 RWMutex 控制并发,两者缺一不可。
选型建议:
- 只要需要按访问频次/时间淘汰(比如 API 响应缓存、用户会话预热),必须用
golang-lru或同类结构,别硬套sync.Map - 如果只是临时存储、无淘汰需求、且 key 基本不重叠(如 per-request metadata),
sync.Map更轻量 -
sync.Map的写开销比普通 map 高,高频写场景下反而不如加锁的普通 map +Mutex
ARC 和 2Q 缓存里的锁升级陷阱
arc/arc.go 和 2q.go 在标准 LRU 上叠加了更复杂的替换逻辑,比如 ARC 需要动态调整 T1/T2 队列长度,2Q 需要维护 A1out 队列。这些操作不再是单次链表调整,而是跨多个子结构的协调更新。
它们依然用 RWMutex,但关键区别在于:所有涉及队列结构调整的操作(如 Add、MoveToMRU)都使用 Lock(),哪怕只是读取某个队列长度——因为长度本身是状态的一部分,和队列内容强耦合。
常见错误:
- 试图把 ARC 的
Get拆成只读操作,用RLock(),结果在判断是否命中 T1 后、还没来得及更新状态时,另一个 goroutine 已修改了 T1 大小,导致状态错乱 - 在自定义
onEvictedCB中调用cache.Get(),形成锁嵌套,可能死锁(RWMutex不支持重入) - 忽略 ARC 的初始化成本:首次
NewARC会分配多个内部队列,且需原子设置初始容量,这部分不在锁保护范围内,但影响后续并发行为
为什么 defer Unlock 不是银弹
defer c.lock.Unlock() 看似稳妥,但它只解决「正常执行路径」下的锁释放。一旦函数里有 return 早于 defer,或者 panic 发生在 defer 注册之后、Unlock 执行之前,锁仍会被持有。
更隐蔽的问题是:Go 的 defer 在函数 return 前执行,但如果锁是在嵌套作用域里获取的(比如在 for 循环内反复 Lock/Unlock),defer 会累积,造成延迟释放甚至锁泄漏。
实操建议:
- 锁的粒度尽量小,避免在大函数里包一堆逻辑;把
Lock()放在最靠近临界区的位置,而不是函数开头 - 对可能 panic 的路径(如回调函数、外部接口调用),显式用
recover()+Unlock(),不要依赖 defer - 用
go vet -race检测数据竞争,但注意它无法发现逻辑错误导致的锁未释放——得靠压测时观察 goroutine block profile
RWMutex 一劳永逸。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











