LRU-2缓存结构需存两级数据:cache1存key→{value, expiry_time}并用哈希表+双向链表维护LRU顺序,cache2存key→{access_count, expiry_time}仅记录元信息;get()须查cache1、查cache2晋升、末尾惰性清理过期项;insert()需限cache2大小并随机剔除;所有时间统一用steady_clock。

LRU-2缓存结构到底要存什么
LRU-2不是标准库里的东西,得自己搭骨架:它本质是两级哈希表 + 双链表组合,第一级(cache1)快速响应,第二级(cache2)兜底并记录访问频次;只有被访问两次的键才晋升到cache1,避免一次性热点污染主缓存。过期支持不能只靠时间戳字段——必须配合定时扫描或惰性淘汰,否则内存会持续增长。
-
cache1存key → {value, expiry_time},用std::unordered_map+ 手动维护std::list做LRU顺序 -
cache2存key → {access_count, expiry_time},只记录元信息,不存真实value - 所有
expiry_time统一用std::chrono::steady_clock::time_point,别用system_clock(时钟回拨会导致误删)
怎么写get()才能正确触发晋升和过期检查
一次get()调用要同时处理三级逻辑:查cache1命中、查cache2晋升、清理过期项。最容易错的是“先清理再查”还是“查完再清理”——必须查完再清理,否则刚被访问的过期项可能被误删。
- 先在
cache1里找,命中则更新cache1的LRU顺序,并检查expiry_time是否已过,过期就erase并返回std::nullopt - 未命中则查
cache2:存在且access_count == 1,就++access_count;若access_count == 2,说明这是第二次访问,立即从cache2移出,插入cache1(带当前value和新expiry) - 每次
get()结尾调用prune_expired(),但只扫描cache2中最近N个项(比如50个),避免单次操作太重
insert()时如何避免cache2无限膨胀
cache2不淘汰就变成内存黑洞,但又不能像cache1那样严格LRU——它的作用是“观察窗口”,需要保留一定历史。折中方案是设硬上限+随机剔除,而不是等满才清。
- 插入新key前,先检查
cache2.size()是否超过阈值(比如10000),超了就遍历一次,用std::uniform_int_distribution随机选10%的项删除 - 不要按访问时间删——
cache2里没存访问时间,只存access_count和expiry_time,删就删expiry_time最旧的那批 - 插入时如果key已在
cache2中,只更新access_count和expiry_time,不增加size
为什么std::list不适合直接当LRU链表用
用std::list<Key>存顺序看似简单,但erase()需要O(n)找节点位置;而LRU要求O(1)定位+O(1)移动。必须配合std::unordered_map<Key, std::list<Key>::iterator>来加速。
立即学习“C++免费学习笔记(深入)”;
- 声明时写:
std::unordered_map<Key, std::list<Key>::iterator> cache1_order;和std::list<Key> lru_list; - 每次
get()命中后,先lru_list.erase(cache1_order[key]),再lru_list.push_front(key),最后更新cache1_order[key] = lru_list.begin() - 别忘了
insert()时也要同步更新cache1_order和lru_list,漏掉一个就会迭代器失效
过期逻辑本身不难,难的是在多级之间保持状态一致——cache2里还在计数的key,cache1里可能已经过期被删了,这种不一致必须靠get()时的惰性修复来收敛。上线前务必用带时间跳跃的单元测试跑一遍。


















