redis采用惰性删除与定期删除双策略:访问键时检查并删除过期key(惰性),同时每秒10次随机抽样清理(定期),兼顾内存效率与cpu开销。

用 threading.Timer 实现单次过期不靠谱
直接为每个 key 启动一个 threading.Timer 看似直观,但实际会快速耗尽线程资源,尤其在高频写入、大量 key 场景下极易触发 RecursionError 或内存暴涨。Timer 本质是延迟执行+销毁,无法动态调整或批量清理,也不支持 TTL 更新。
实操建议:
- 避免对每个 key 单独建 Timer,哪怕用线程池也难控生命周期
- 若仅需极简原型(比如测试用),可用
heapq+ 单个后台线程轮询最小堆,但务必加锁保护过期检查和删除操作 - 真正要接近 Redis 行为,必须引入“惰性删除 + 定期抽样”双策略,否则无法兼顾精度与性能
用 heapq 维护过期时间堆 + 后台线程抽样清理
核心思路是把所有带 TTL 的 key 按过期时间戳存进最小堆(heapq),后台线程定期 peek 堆顶,若已过期则 pop 并删除对应 key;同时每次 get/set 操作都顺手检查堆顶是否过期(惰性删除)。
关键细节:
- 堆中存的是
(expire_timestamp, key)元组,注意 timestamp 必须是绝对时间(如time.time() + ttl_sec),不能存相对值 - 每次
set(key, value, ex=60)时,先从堆中移除旧的同 key 过期项(需额外维护key → expire_timestamp映射,否则无法精准删除) - 后台线程 sleep 间隔建议设为 100ms~1s,太短空转浪费 CPU,太长导致过期延迟明显
- 抽样清理时别一次删光——Redis 默认每次只检查 20 个随机 key,你也该限制单次最多处理
min(20, len(heap))
__delitem__ 和 __getitem__ 里必须做惰性过期检查
用户调用 cache['key'] 或 del cache['key'] 时,不能假设 key 还有效。哪怕后台线程还没扫到它,也可能已超时。
实操要点:
- 在
__getitem__开头立刻查该 key 是否在过期映射中,且expire_timestamp ;若是,先 <code>del再抛KeyError -
__delitem__同理:先检查是否过期,再从数据字典和过期堆中同步清理(注意堆无法高效删指定元素,所以依赖“标记失效+后续跳过”的方式更稳妥) - 不要在
__setitem__里主动触发清理——那是后台线程的事;但要确保新 TTL 覆盖旧记录,包括更新堆和映射表
用 weakref 或 __slots__ 控制内存膨胀风险
长期运行的服务里,如果 key 数量大、TTL 长短不一,过期堆和映射表本身就会成为内存瓶颈。单纯靠清理逻辑不够,得从结构上约束。
推荐做法:
- 给缓存类加上
__slots__ = ('_data', '_expires', '_heap', '_lock'),禁用动态属性,减少单实例内存占用 - 不用
dict存全部 key 的过期时间,改用weakref.WeakKeyDictionary(如果 key 是可哈希对象)或weakref.WeakValueDictionary(如果 value 可被回收) - 堆中不存原始 key 对象,而存其 hash 或 id,配合外部字典做间接引用,避免循环引用阻碍 GC
过期机制真正的复杂点不在“怎么删”,而在“删不干净时怎么不让系统变慢”。堆的修复、锁粒度、GC 友好性,这些才是压测时最先暴露的问题。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











