lru-2缓存结构是两级链表:access_list仅存key(限长,如200),记录访问热度;main_list存完整键值对及过期时间;通过access_map和main_map分别映射迭代器,实现o(1)定位与协同操作。

LRU-2缓存结构到底长什么样
LRU-2不是标准库里的东西,它本质是两级链表:第一级叫access_list,记录最近两次访问的key;第二级叫main_list,存所有缓存项。一个key只有在access_list里出现过**两次**(即被访问过两轮),才会晋升进main_list;反之,如果长期只在access_list里“晃荡”却没再被访问,就会被淘汰。这种设计能过滤掉偶然访问的噪声,比单级LRU更抗抖动。
关键点在于:两个链表都得支持O(1)插入、移动和删除——所以必须用std::list + std::unordered_map组合,map存迭代器,避免遍历找节点。
如何用std::list + std::unordered_map实现双链表协同
不能共用一个std::list<cacheentry></cacheentry>然后靠标记区分状态,那样删除时无法保证O(1)。必须分开维护:
-
std::list<:string> access_list</:string>:只存key,不存value,长度严格限制(比如最多200个) -
std::list<cacheentry> main_list</cacheentry>:存完整键值对,带时间戳用于过期判断 -
std::unordered_map<:string std::list>::iterator> access_map</:string>:快速定位key在access_list中的位置 -
std::unordered_map<:string std::list>::iterator> main_map</:string>:快速定位key在main_list中的位置
每次get(key)时:先查main_map,命中就更新main_list顺序并返回;没命中则查access_map,存在就把它移到access_list头部;如果不存在,就插入到头部,并检查是否已达上限——超了就删尾部,同时从access_map里erase对应项。
什么时候把key从access_list“晋升”到main_list
晋升动作发生在get(key)且该key已在access_list中存在时。但注意:不是每次命中都晋升,而是要确保这个key在access_list里**至少被访问过两次**(即上次访问后它没被踢出)。实现上最稳的方式是:第一次访问→插入access_list;第二次访问→从access_list中erase它,并插入main_list(同时写入main_map)。
伪逻辑如下:
if (main_map.find(key) != main_map.end()) {
// 命中主缓存:更新顺序,返回
} else if (access_map.find(key) != access_map.end()) {
// 在access_list中:擦除它,插入main_list,更新两个map
access_list.erase(access_map[key]);
access_map.erase(key);
main_list.push_front({key, value, now()});
main_map[key] = main_list.begin();
} else {
// 全新key:插入access_list头部
access_list.push_front(key);
access_map[key] = access_list.begin();
}
这里容易漏掉的是:插入main_list前,要确保value已加载(比如从DB或下游服务读取),否则缓存的是空值。
过期清理怎么跟LRU-2逻辑不打架
过期不能靠定时器轮询——会拖慢get响应,也不准。推荐在每次get或put时顺手清理:遍历main_list尾部,删掉所有expire_time 的项,直到遇到未过期项为止。注意别用<code>while (!main_list.empty() && main_list.back().expire_time 直接删,因为<code>main_list.back()可能被main_map引用着,必须先从map里erase key,再erase list节点。
具体步骤:
- 从
main_list尾部开始迭代 - 对每个节点,检查
it->expire_time - 若过期:调用
main_map.erase(it->key),再调用main_list.erase(it) - 遇到第一个未过期节点就停,避免全量扫描
access_list不设过期——它只负责“访问热度”信号,本身不存value,过期逻辑只作用于main_list。
真正麻烦的是并发场景:如果没加锁,access_list和main_list的迭代器可能失效。哪怕只读操作也要考虑std::shared_mutex,尤其当put可能触发晋升时,两个链表+两个map的操作必须原子。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











