不能直接用sync.Map实现LRU,因其不记录访问顺序、Range遍历无序、无容量控制与淘汰钩子,且LoadOrStore无法原子完成“查+移至链表头”,导致淘汰随机、内存持续增长至OOM。

为什么不用 sync.Map 直接当 LRU 用
sync.Map 本身不维护访问顺序,Get 后无法原子地把键“移到最近使用位置”——这是 LRU 的核心动作。它适合读多写少、无序场景(比如配置快照),但一旦需要淘汰最久未用项,你就得自己遍历所有 key 做时间戳比对,复杂度退化成 O(n),压测时延迟直接跳变。更麻烦的是:sync.Map 的 Range 遍历不保证顺序,且删除后 key 可能残留在 dirty map 里长期不回收,缓存 size 会悄悄膨胀。
container/list + map 组合必须这样初始化
标准库的 list.List 是线程不安全的,但它和 map 搭配能做出真正 O(1) 的 LRU。关键不是“能不能跑”,而是三处初始化不能漏:
-
map[string]*list.Element必须显式make,不能只声明;否则Put时delete(cache, key)会 panic -
list.List要配前后哨兵节点(head和tail),避免空指针判断;head.Next = tail、tail.Prev = head这两行不能少 - 每个
*list.Element的Value必须是自定义结构体(如type entry struct { key string; value interface{} }),不能直接存value—— 否则淘汰时无法从Element.Value反查key去清理map
并发锁粒度:别信“读用 RLock 就够了”的说法
list.MoveToFront 看似只是移动指针,但它修改链表结构,sync.RWMutex.RLock() 保护不了。实测中只要 Get 里用 RLock 包住 map 查找 + MoveToFront,两个 goroutine 同时命中同一 key,就会触发 panic: assignment to entry in nil map 或链表断裂。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
更稳妥的做法是:
立即学习“go语言免费学习笔记(深入)”;
- 所有公开方法(
Get/Put/Remove)统一用sync.Mutex,简单、可预测、压测不抖动 - 真要扛高并发读?做分片(shard):按
key哈希到 8 或 16 个独立LRUCache实例,每片一把锁,吞吐线性提升 - 绝对不要在锁内调用任何可能阻塞的逻辑(比如
http.Get、time.Sleep、数据库查询)——缓存层只负责搬运,加载逻辑应由上层兜底
淘汰逻辑失效的三个隐蔽原因
容量设为 100,实际存了 120 条还删不掉?问题往往出在淘汰步骤的顺序和生命周期管理上:
- 删节点必须“先清
map,再删list”:正确顺序是delete(l.cache, ent.key); l.list.Remove(e)。反过来会导致map里残留指向已释放Element的指针,后续Get可能读到脏数据或 panic -
list.Element不是线程安全的,哪怕只读其Value字段也必须在锁内——因为另一 goroutine 可能在你读完瞬间调Remove,让e.Value变成nil - 用
interface{}存值时,Get返回前必须加ok判断:if v, ok := e.Value.(string); ok { return v }。否则存int却断言成string,静默返回零值,缓存“生效”了,数据却丢了
Put”和“多个 goroutine 同时 Get 未命中 key”的组合场景——单元测试只跑单线程成功案例,上线后才暴露竞态和淘汰失效。


















