不能用 std::list + std::unordered_map 直接套标准 lru 模式,因为 lru-2 淘汰依据是倒数第二次访问时间(prev_access),而 std::list 无法在 o(1) 内按 prev_access 重排,遍历找最小值退化为 o(n),且无法表达“刚被访问两次但未晋升”的中间状态。

为什么不能用 std::list + std::unordered_map 直接套标准 LRU 模式
因为 LRU-2 的淘汰依据不是「最后一次访问时间」,而是「倒数第二次访问时间」(prev_access)。std::list 只能反映单次访问顺序,节点里不存历史时间戳,也没法在 O(1) 时间内按 prev_access 重排——每次淘汰前都得遍历全表找最小值,复杂度退化成 O(n)。更麻烦的是,它无法表达「刚被访问两次,但还没来得及晋升」这种中间状态,硬塞会导致误淘汰或晋升延迟。
核心结构怎么组织:两个哈希表 + 单链表 + steady_clock
LRU-2 不是双链表嵌套,而是两级观察+响应结构:
-
cache_:主缓存层,std::unordered_map<key std::pair timepoint>></key>,存值和最新访问时间last_access -
history_:观察层,std::unordered_map<key timepoint></key>,只存每个 key 的上一次访问时间(即prev_access),不存 value -
lru_list_:可选的std::list<key></key>,仅用于快速把命中项移到头部(配合key_to_iter_哈希表实现 O(1) 定位);若不用链表,就靠history_遍历淘汰 - 所有时间统一用
std::chrono::steady_clock::time_point,别碰system_clock——系统时间跳变会直接导致大批缓存误删
get() 和 put() 中必须同步处理的三件事
一次 get() 不是查完就完,漏掉任一环节都会破坏 LRU-2 语义:
- 先查
cache_:命中则检查now() - last_access > ttl_,过期就 erase 并返回空;未过期则把当前时间写入history_[key],再更新cache_[key].second - 未命中则查
history_:存在且history_[key]不是默认构造值(即已记录过一次访问),说明这是第二次访问 → 立即从history_移出,构造新 entry 插入cache_,并设expire_time = now() + ttl_ - 结尾调
prune_expired(),但只扫描history_最近 50 个 key(惰性清理),避免单次操作卡顿;注意:必须查完再清,否则刚被访问的过期项可能被删掉,导致晋升失败
容易被忽略的三个内存与时间陷阱
这些错误不会编译报错,但上线后会悄无声息地泄漏内存或提前淘汰有效项:
-
get()后没调touch()更新last_access和prev_access→ 缓存项静默过期,用户看到“刚存进去就取不到” -
history_没设 size 上限,也不做随机剔除 → 长期运行后变成内存黑洞;推荐硬限 10000,超限时用std::uniform_int_distribution随机删约 10% 的项 -
std::list::erase(iterator)后立刻用该 iterator 访问 map → 迭代器已失效,后续get()可能解引用野指针;正确做法是 erase 前先保存 key 值,再通过 key 删除 map 条目
真正难的不是写对逻辑,而是让 history<em></em> 的清理节奏和 cache 的晋升时机严丝合缝——差几毫秒,就可能把一个本该稳住的热 key 当冷数据踢掉。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











