lru-2区别于标准lru的核心在于:它记录每个缓存项的最近两次访问时间戳,淘汰时优先移除“第二次最近访问时间最早”的项,而非仅依据最后一次访问时间;这使其能有效过滤单次突发访问,显著提升抗缓存污染能力。

组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
LRU-2和标准LRU的区别在哪?
LRU-2不是简单的“两次访问才保留在缓存中”,而是记录每个缓存项的**最近两次访问时间戳**,淘汰时优先踢掉「第二次最近访问时间最早」的项。它比LRU更抗突发流量干扰,比如某个key被刷一次就进缓存、再刷一次就稳住,而LRU可能刚进就被挤走。
标准std::list + std::unordered_map结构没法直接支持双时间戳管理,硬塞两个time_t字段会增加节点体积,且排序逻辑变复杂——不能只靠链表顺序,得额外维护一个按「第二近访问时间」组织的索引。
用两个std::list实现双时间维度管理
核心思路是拆开职责:一个链表按**最新访问时间**维护(用于快速更新),另一个按**第二次最近访问时间**维护(用于淘汰)。每次get()或put()命中时:
- 从「最新链表」中摘下该节点,插到头部(表示最新)
- 如果该节点在「第二近链表」中已存在,先把它删掉;然后把当前「最新链表中它前一个节点的时间戳」作为它的第二近时间,插入到「第二近链表」对应位置
- 如果这是第一次访问,「第二近时间」设为0,插入到「第二近链表」尾部(保证新项最晚被淘汰)
这样避免了单节点存两个时间戳+重排序的开销,也规避了std::set或std::priority_queue带来的O(log n)更新成本。
std::list节点必须带迭代器指针
C++里std::list::erase()需要迭代器,但你在「第二近链表」里删一个节点时,根本不知道它在那个链表里的位置——除非你提前存好。所以每个缓存value类型得包装一层:
struct CacheNode {
int key;
int value;
std::list<cachenode>::iterator latest_it; // 指向最新链表中的位置
std::list<cachenode>::iterator second_it; // 指向第二近链表中的位置
time_t last_access; // 最近一次访问时间(可选,调试用)
};
</cachenode></cachenode>
注意:latest_it和second_it不能是std::list::iterator裸存,因为std::list重排不使迭代器失效,但erase()后必须重置。每次移动节点前后,都要手动更新这两个字段。
淘汰时别直接pop_back()第二近链表
你以为「第二近链表」尾部就是该淘汰的?不一定。因为:
- 新插入的项第二近时间为0,会被排到最后,但它其实没被访问过第二次
- 多个项可能共享同一「第二近时间」,需稳定排序(比如按key或插入顺序)
- 如果某项被
put()覆盖,它的「第二近时间」应失效,但旧节点还挂在链表里
实际做法是:淘汰前先遍历「第二近链表」尾部若干项(比如3个),对每个检查其second_it是否仍有效(可通过反向查map确认该节点是否还在缓存中),再挑出真正有效的、第二近时间最小的那个。这步O(1)均摊,但写错容易导致内存泄漏或重复淘汰。
LRU-2真正麻烦的不是逻辑,而是两个链表+双迭代器+生命周期同步——稍不注意,erase()后忘了清迭代器,下次访问就Segmentation fault。
std::list实现双时间维度管理
核心思路是拆开职责:一个链表按**最新访问时间**维护(用于快速更新),另一个按**第二次最近访问时间**维护(用于淘汰)。每次get()或put()命中时:
- 从「最新链表」中摘下该节点,插到头部(表示最新)
- 如果该节点在「第二近链表」中已存在,先把它删掉;然后把当前「最新链表中它前一个节点的时间戳」作为它的第二近时间,插入到「第二近链表」对应位置
- 如果这是第一次访问,「第二近时间」设为0,插入到「第二近链表」尾部(保证新项最晚被淘汰)
std::set或std::priority_queue带来的O(log n)更新成本。
std::list节点必须带迭代器指针
C++里std::list::erase()需要迭代器,但你在「第二近链表」里删一个节点时,根本不知道它在那个链表里的位置——除非你提前存好。所以每个缓存value类型得包装一层:
struct CacheNode {
int key;
int value;
std::list<cachenode>::iterator latest_it; // 指向最新链表中的位置
std::list<cachenode>::iterator second_it; // 指向第二近链表中的位置
time_t last_access; // 最近一次访问时间(可选,调试用)
};
</cachenode></cachenode>
注意:latest_it和second_it不能是std::list::iterator裸存,因为std::list重排不使迭代器失效,但erase()后必须重置。每次移动节点前后,都要手动更新这两个字段。
淘汰时别直接pop_back()第二近链表
你以为「第二近链表」尾部就是该淘汰的?不一定。因为:
- 新插入的项第二近时间为0,会被排到最后,但它其实没被访问过第二次
- 多个项可能共享同一「第二近时间」,需稳定排序(比如按key或插入顺序)
- 如果某项被
put()覆盖,它的「第二近时间」应失效,但旧节点还挂在链表里
实际做法是:淘汰前先遍历「第二近链表」尾部若干项(比如3个),对每个检查其second_it是否仍有效(可通过反向查map确认该节点是否还在缓存中),再挑出真正有效的、第二近时间最小的那个。这步O(1)均摊,但写错容易导致内存泄漏或重复淘汰。
LRU-2真正麻烦的不是逻辑,而是两个链表+双迭代器+生命周期同步——稍不注意,erase()后忘了清迭代器,下次访问就Segmentation fault。
pop_back()第二近链表
你以为「第二近链表」尾部就是该淘汰的?不一定。因为:
- 新插入的项第二近时间为0,会被排到最后,但它其实没被访问过第二次
- 多个项可能共享同一「第二近时间」,需稳定排序(比如按key或插入顺序)
- 如果某项被
put()覆盖,它的「第二近时间」应失效,但旧节点还挂在链表里
second_it是否仍有效(可通过反向查map确认该节点是否还在缓存中),再挑出真正有效的、第二近时间最小的那个。这步O(1)均摊,但写错容易导致内存泄漏或重复淘汰。
LRU-2真正麻烦的不是逻辑,而是两个链表+双迭代器+生命周期同步——稍不注意,erase()后忘了清迭代器,下次访问就Segmentation fault。C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!










