应使用 std::chrono::steady_clock::time_point 存储时间戳,避免 system_clock 和手动秒数计算;steady_clock 不受系统时间调整影响,可防止缓存误失效或永不过期。

用 std::chrono 存时间戳,别手写秒数计算
直接存 std::chrono::steady_clock::time_point,不是 time_t 或毫秒整数。前者能避免时钟跳变、夏令时干扰,且 steady_clock 不受系统时间修改影响——这点在缓存过期判断里很关键。
常见错误是用 system_clock::now() + 手动加秒数,结果一调系统时间,所有缓存瞬间失效或永不过期。
- 过期时间统一用
steady_clock::now() + std::chrono::seconds(300)这种方式生成 - 读取时用
if (entry.expiry_time 判断,不转成秒再比较 - 不要把
time_point转成字符串或整数存,会丢精度、增开销
清理线程别轮询 sleep(1),改用条件变量+超时等待
每秒检查一次所有缓存项?CPU 白耗,尤其空闲时。更糟的是,如果某次清理卡住,后续检查全错位。
正确做法是让清理线程“等最近一个缓存到期”,而不是固定间隔唤醒。
立即学习“C++免费学习笔记(深入)”;
- 维护一个最小堆(比如
std::priority_queue),按expiry_time排序 - 线程用
cv.wait_until(lock, next_expiry)等到下一个过期点,而不是sleep - 每次插入新缓存时,通知条件变量(
cv.notify_one()),以防新项比当前等待时间更早到期 - 注意:堆里存的得是迭代器或句柄,不能存裸指针,避免清理时迭代器失效
多线程读写缓存必须分离锁粒度
一个大互斥锁包住整个缓存容器?读多写少场景下,get() 会被 cleanup() 或另一个 set() 长期阻塞,吞吐直接掉一半。
真正需要互斥的只是“修改容器结构”和“更新单个 entry 的 expiry_time”,读取 value 可以无锁(配合原子标记或 RCU 思路)。
- 用
shared_mutex(C++17):读用lock_shared(),写/清理用lock() - 清理线程只在真正删除节点时才独占锁;扫描过期项可先用共享锁批量收集待删 key,再换独占锁删
- 插入时若 key 已存在,只更新 value 和
expiry_time,不重新分配节点——减少锁争抢
std::map 和 std::unordered_map 选哪个?看是否要按时间排序
如果清理线程依赖“最早过期项”,用 std::map 按 expiry_time 排序最省事;但查找 key 就变成 O(log n)。反过来,如果 get()/set() 极其频繁,而清理可以容忍延迟几秒,那就用 unordered_map + 单独维护一个 std::priority_queue<expiry_time key></expiry_time>。
坑在于:优先队列没法高效删中间项。所以当某个 key 被更新过期时间,旧的过期记录还在堆里,得用“懒删除”——出堆时检查该 key 当前 expiry 是否匹配。
- 堆里每个元素带一个版本号或序列号,entry 结构里存最新版本,不匹配就跳过
- 或者用
std::set<:pair key>></:pair>,支持 O(log n) 插入/删除/查最小,但内存略高 - 别用
multiset存重复 time_point —— 同一时刻多个缓存过期时,key 无法区分,删错项
最易被忽略的是清理线程和业务线程对同一 entry 的生命周期竞争:清理线程刚标记待删,业务线程又在读 value。value 必须确保在清理完成前不析构,要么用 std::shared_ptr,要么把数据拷贝出来再删节点。










