不能用标准container/list+map直接实现lru-k,因其需双链表+双map+独立计数器+懒删除最小堆+内存压力感知等五要素物理隔离;混用会导致晋升门槛失效、历史队列误淘汰(应front而非back)、cnt与history.mp不同步,根本原因是lru-k本质是fifo暂存层与lru服务层的逻辑分离,非简单lru加计数。

不能用标准 container/list + map 直接拼出可用的 LRU-K,它必须拆成两套物理隔离结构,否则晋升逻辑失效、历史队列膨胀、淘汰错乱。
为什么 container/list 无法直接支撑 LRU-K
常见错误是把 LRU 的 MoveToFront 搬过来,再加个计数器就叫 LRU-K。这会导致:
-
Get一次就进主缓存,完全跳过「访问 K 次才晋升」门槛 - 历史队列(
historyCache.ll)误用Back()淘汰,实际该用Front()—— 它是 FIFO,不是 LRU -
cnt和historyCache.mp不同步:key 已从 history 链表Remove,但计数器没删,后续再Get会误判晋升条件
根本原因在于:LRU-K 不是「LRU + 计数」,而是两个逻辑层——未达标的暂存层(FIFO)、已达标的服务层(LRU),必须物理隔离。
双链表 + 双 map + 计数器怎么组织才不出错
必须拆成两组互不干扰的结构:
- 主缓存层:
ll *list.List+mp map[string]*list.Element,行为和标准 LRU 一致;Get命中后调ll.MoveToFront() - 历史层:
historyCache.ll *list.List+historyCache.mp map[string]*list.Element+historyCache.cnt map[string]int;Get未命中时,先PushBack到 history 链表尾,再cnt[key]++ - 晋升触发点只在
Get:若cnt[key] >= k,则从historyCache.ll中Remove对应节点,再走一次Put(key, value)进主缓存 -
historyCache.ll的淘汰必须用Front()(保留最老访问记录),而主缓存淘汰用Back()
如何高效选出「第 K 次访问最久」的 key
不能每次淘汰都遍历所有 key 并取 history[0](O(N) 太重)。正确路径是:
- 为每个 key 维护一个长度 ≤ K 的升序
accessHistory []time.Time,插入时append(history, now),超长则history = history[1:];这样history[0]就是「第 K 次访问时间」 - 用
container/heap构建最小堆,元素为{key string, kthTime time.Time},按kthTime排序 - 每次 key 的
accessHistory更新后,把新kthTime推入堆 —— 允许重复 key 入堆 - 淘汰时循环
Pop()堆顶,检查其kthTime是否等于当前 key 的history[0];不等就丢弃(懒删除),直到拿到有效项
并发安全下锁怎么分层才不卡 Get
LRU-K 比纯 LRU 多一层 history 访问,锁粒度更敏感。不能所有操作都用一把 sync.Mutex:
- 主缓存读(
Get命中路径):用sync.RWMutex.RLock(),只保护mp查找和ll.MoveToFront() - 主缓存写(
Put/RemoveOldest):升级为Lock(),但临界区只做链表指针重连 +mp增删,绝不含 value 序列化、日志、回调 - history 部分:单独一把
sync.RWMutex(比如叫historyMu),因为它的读写频率和主缓存不一致;Get未命中时先historyMu.RLock()查cnt,再Lock()更新计数或移出队列 - 切忌:在
Get中先Unlock()主锁,再去处理 history —— 这会导致竞态
真正难的不是写对单条路径,而是让历史队列的 FIFO 行为、晋升阈值的原子性、堆中懒删除的时效性,在高并发下同时成立——任何一个环节松动,冷热判断就失效。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











