直接用 dict 加时间戳无法实现高效 ttl,因需手动遍历判断和清理,易致性能下降与内存泄漏;应采用惰性淘汰+主动清理组合策略。

为什么不能直接用 dict 加时间戳模拟 TTL?
因为 dict 本身不感知过期,查的时候得手动判断每个键的时间戳,删的时候得遍历清理——这在高频读写或大量 key 场景下会明显拖慢性能,还容易漏删导致内存泄漏。真正需要的是“访问时惰性淘汰 + 定期/按需主动清理”的组合策略。
threading.Timer 和 heapq 搭配实现轻量 TTL 字典
适合中小规模、对精度要求不苛刻(允许误差 ±1 秒)的场景。核心思路:用最小堆维护过期时间戳,每次写入启动一个延迟清理的 Timer;读取时检查是否已过期,过期则删除并返回 None。
关键点:
- 每个 key 对应一个
threading.Timer,大量 key 时线程开销不可忽视 - 堆里存的是
(expire_at, key),必须保证expire_at是绝对时间(如time.time() + ttl) - 写入重复 key 时,要 cancel 原 timer 并 push 新 entry,否则旧 timer 仍会触发删除
- 读操作不重置 TTL,若需 LRU+TTL 混合行为,得额外维护访问时间
import time
import heapq
import threading
<p>class TTLDict:
def <strong>init</strong>(self):
self._data = {}
self._heap = [] # [(expire_at, key)]
self._lock = threading.Lock()</p><pre class="brush:python;toolbar:false;">def __setitem__(self, key, value):
with self._lock:
expire_at = time.time() + 300 # 默认 5 分钟
heapq.heappush(self._heap, (expire_at, key))
# 启动清理 timer(实际中建议用 weakref 或统一调度器避免强引用)
timer = threading.Timer(300, self._cleanup_one, args=(key,))
timer.daemon = True
timer.start()
self._data[key] = (value, expire_at)
def __getitem__(self, key):
with self._lock:
if key not in self._data:
raise KeyError(key)
value, expire_at = self._data[key]
if time.time() > expire_at:
del self._data[key]
return None
return value
def _cleanup_one(self, key):
with self._lock:
self._data.pop(key, None)
用 redis-py 的 EXPIRE 实现生产级 TTL 字典
当数据量大、需跨进程共享、或要求精确过期(毫秒级)、自动后台清理时,别自己造轮子——直接用 Redis。Python 侧只需封装一层语义,底层交给 redis-py 的原子命令。
实操要点:
-
r.set(key, value, ex=300)是最简方式,ex单位为秒,支持exat(绝对时间戳)和px(毫秒) - 读取前不用手动检查过期,
r.get(key)返回None即代表已过期或不存在 - 批量操作慎用
pipeline:Redis 的过期是 key 粒度的,pipeline 中多个SET+EXPIRE可能因网络中断导致部分 key 无过期时间 - 本地开发可用
redis-server --port 6380起个临时实例,避免污染主环境
用 cachetools.TTLCache 快速落地,但注意它的淘汰逻辑
这是最省事的选择,尤其适合单进程、缓存类场景。但它不是“字典替代品”,而是带 TTL 的 LRU 缓存——意味着容量满时会按最近最少使用淘汰,且过期检查只在 __getitem__ 时做(惰性),不会主动扫描。
容易踩的坑:
-
TTLCache(maxsize=128, ttl=300)中maxsize是硬上限,过期 key 仍占位置,直到被访问或被新 key 挤出 - 没有
keys()或items()的实时快照,遍历时可能遇到已过期但尚未清理的项 - 多线程安全,但高并发下
__getitem__的锁竞争会影响吞吐,压测时留意cache_info()的currsize和maxsize差距
如果只是想让某个配置字典自动失效,又不想引入 Redis,cachetools.TTLCache 就够用了;但如果要完整字典接口(比如支持 .popitem()、迭代所有未过期项),就得自己补全或换方案。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











