lru-2是基于双时间窗口的两级缓存淘汰策略:条目需在活跃窗口内被访问至少两次才升入热区,否则滞留冷区并按lru快速淘汰;其本质区别在于依赖时间戳判断访问模式而非简单计数,从而抗抖动更强。

什么是LRU-2,它和普通LRU有什么本质区别
LRU-2不是“两次访问就提权”的简单变种,而是基于访问历史的双时间窗口淘汰策略:一个条目必须被**至少访问两次**,且**两次访问间隔不超过某个阈值(即“活跃窗口”)**,才进入二级缓存(热区);否则即使访问过一次,也只留在一级缓存(冷区),并按LRU快速淘汰。它的核心目标是过滤掉偶发、试探性访问,比单次LRU更抗抖动。
关键点在于:不能只靠计数器——access_count 无法区分“1秒内访问2次”和“1小时后又访问1次”。必须记录最近两次访问时间戳,或至少保留最近一次访问时间 + 一个“是否已在热区”的状态位。
用std::unordered_map + std::list实现两级链表结构
常见错误是试图用一个链表模拟两级——结果插入/移动逻辑爆炸式复杂。正确做法是维护两个独立但联动的双向链表:
-
std::list<key></key>m_cold_list:一级缓存链表,只存最近一次访问过的冷数据;头插尾删,满时直接 pop_back -
std::list<key></key>m_hot_list:二级缓存链表,存通过“双访问验证”的热数据;同样头插尾删,但淘汰前需检查过期 -
std::unordered_map<key std::pair std::chrono::steady_clock::time_point>></key>存所有数据 + 最后访问时间;再配一个std::unordered_map<key bool></key>或用std::optional<:chrono::steady_clock::time_point></:chrono::steady_clock::time_point>标记“是否已记录首次访问”
每次 get(key) 时:若 key 在 m_hot_list 中,移到头部并更新时间;若在 m_cold_list 中,检查上次访问距今是否 ≤ 窗口(如 5s),是则移入 m_hot_list,否则仅更新时间并重置到 m_cold_list 头部;若不在任何列表中,插入 m_cold_list 头部。
如何安全地处理过期与并发访问
LRU-2的过期不是TTL式全局失效,而是“热区条目自上次访问起超时即降级回冷区”。这意味着你不能依赖后台线程定期扫描——可能刚标记过期就被新请求命中,又升回热区,造成误删。
推荐在每次 get 或 put 时做惰性检查:
- 访问
m_hot_list头部前,先用std::chrono::steady_clock::now()检查其对应时间戳是否超时;若超时,pop_front 并插入m_cold_list头部 - 避免在链表遍历中 erase —— 先收集待降级 key,再批量处理,防止迭代器失效
- 如果需要多线程安全,不要粗暴加全局锁;对
m_cold_list和m_hot_list分别加std::shared_mutex,读多写少场景下性能更好
注意:std::chrono::steady_clock 是唯一可信赖的时间源,system_clock 可能因NTP校正跳变,导致大量条目误判过期。
为什么不用std::map<time_point key></time_point>替代链表
有人想用红黑树按时间排序自动管理过期,看似优雅,实则埋坑:
- 每次
get都要从树中 erase 旧时间点 + insert 新时间点,O(log n) 开销翻倍,而链表头插是 O(1) - 树无法高效支持“按访问频次升降级”逻辑——LRU-2 的核心是访问模式判断,不是单纯时间排序
- 过期检查仍需遍历树首节点,但链表只需看 front(),常数级更快
真正需要时间维度的,只在判断“两次访问是否够近”和“热区是否过期”这两个点上,其余全部交给链表位置隐式表达访问序。过度设计容器只会让逻辑散落在各处,难以调试。
最易被忽略的是“窗口时间”的单位一致性:所有 std::chrono 操作必须统一用 std::chrono::seconds 或 std::chrono::milliseconds,混用 duration_cast 容易因截断丢失精度,导致本该升级的条目卡在冷区。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











