最省事方案是使用ttlcache库,它轻量可靠、线程安全、支持惰性淘汰与后台清理;需注意maxsize是LRU容量上限而非TTL数量上限,过期条目仍占位直至访问或显式clean,多线程下应避免频繁调用clean。

用 ttlcache 库最省事,但得注意线程安全和初始化时机
直接上结论:别自己手写带 TTL 的字典,ttlcache(即 ttldict 的现代替代)是目前最轻量、可靠的选择。它底层用 heapq 管理过期时间,支持自动惰性淘汰(访问时检查)+ 后台清理(可选),比基于 threading.Timer 或轮询的方案更准、更省内存。
安装:pip install ttlcache
常见踩坑点:
-
TTLCache(maxsize=128, ttl=30)中的maxsize是 LRU 容量上限,不是 TTL 数量上限;TTL 到期条目会随访问被自动清理,但不会主动触发 LRU 驱逐 —— 换句话说,过期条目仍占着maxsize位置,直到被访问或调用clean() - 多线程环境下默认线程安全,但如果你在多个线程中频繁调用
cache.clean(),可能引发锁竞争;建议只让一个后台线程定期调用(比如每 10 秒一次) - 初始化时传入的
ttl是秒级浮点数,ttl=0.1合法,但小于 0.01 秒的实际精度受限于 Python 的time.time()分辨率和 GIL 调度
手动实现简易版要绕开 time.time() 单调性问题
如果因合规或极简依赖要求必须自研,核心陷阱不在逻辑,而在时间判断本身:用 time.time() 做过期判断,遇到系统时间回拨(NTP 校正、虚拟机休眠恢复)会导致大量条目“假过期”甚至永远不触发淘汰。
正确做法是改用 time.monotonic() 记录插入时刻:
from time import monotonic
<p>class TTLDict:
def <strong>init</strong>(self, default_ttl: float):
self._data = {}
self._default_ttl = default_ttl</p><pre class="brush:php;toolbar:false;">def __setitem__(self, key, value):
self._data[key] = (value, monotonic())
def __getitem__(self, key):
val, inserted_at = self._data[key]
if monotonic() - inserted_at > self._default_ttl:
del self._data[key]
raise KeyError(key)
return val
def get(self, key, default=None):
try:
return self[key]
except KeyError:
return default注意:monotonic() 不可逆、无单位、仅用于差值计算,不能转成“绝对时间”,所以无法支持“指定某个具体时间点过期”这种需求。
redis-py 作为外部 TTL 字典时,别忽略连接池和序列化开销
当数据量大、需跨进程/服务共享、或 TTL 精度要求高(毫秒级)时,直接用 Redis 更稳。但容易低估两个成本:
- 每次
get都是网络往返,哪怕本地 Redis(localhost:6379)延迟也在 0.1–0.5ms 量级;高频小键值场景下,这比内存字典慢 2–3 个数量级 -
redis.Redis().set('k', 'v', ex=60)中的ex是秒,若需毫秒级,得用px=60000;且所有值必须是 bytes 或能被str()转换的类型,datetime、嵌套 dict 等需手动json.dumps(),反序列化失败会静默返回None - 没配连接池(
ConnectionPool)的话,每个新实例都建新连接,短生命周期脚本容易触发ConnectionRefusedError
混合方案:内存字典 + 定时扫描淘汰,适合长 TTL + 低更新频次
比如配置缓存(TTL 几分钟到几小时)、功能开关状态(TTL 数小时),更新不频繁但要求响应极快,这时可以接受“过期后最多延迟 N 秒才真正删除”。关键是要避免全量扫描阻塞主线程:
- 把键按哈希分桶(如
hash(key) % 8),每次只扫一个桶,8 次循环覆盖全量 —— 这样单次扫描耗时可控 - 用
threading.Timer触发扫描,但别设太短(如 1 秒),否则定时器本身调度开销反超收益;建议间隔 ≥ TTL / 10 - 扫描时用
list(dict.keys())避免RuntimeError: dictionary changed size during iteration - 不要在扫描中做复杂逻辑(如日志记录、网络请求),只做
if now - inserted_at > ttl: del d[key]
真正难的是权衡:TTL 越短、数据越热,越该用 ttlcache;TTL 越长、更新越少、一致性要求越低,越适合混合扫描。没有银弹,只有 trade-off。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











