lru_cache不支持TTL,手动加时间判断会破坏原子性;ttlcache库专为LRU+TTL设计,自动过期检查;手写需耦合访问顺序、过期时间和访问时间;Redis缺精确LRU语义。

为什么不能直接用 lru_cache 加手动过期?
functools.lru_cache 本身不支持 TTL(Time-To-Live),它只按访问频次淘汰,无法自动清理“过期但未被驱逐”的条目。你可能试过在装饰函数里加时间判断,但这样会破坏缓存原子性:并发调用时可能重复计算、覆盖过期状态,且无法统一管理过期键。
用 ttlcache 库最省事
Python 生态里 ttlcache 是专为 LRU + TTL 设计的轻量库,内部用 OrderedDict 维护访问顺序,每个条目自带 expires_at 时间戳,get/set 时自动检查并清理过期项。
- 安装:
pip install ttlcache - 基础用法:
from ttlcache import TTLCache
cache = TTLCache(maxsize=128, ttl=60) # 60秒过期 cache['key'] = 'value' print(cache['key']) # 命中返回;超时后 KeyError
- 注意:
ttlcache的maxsize是硬上限,满时按 LRU 清理最久未用项,**不管是否已过期**;过期检查只在__getitem__或get时触发,不是后台定时扫描
自己实现要注意三个关键点
如果必须手写(比如嵌入无 pip 环境、或需定制淘汰逻辑),核心是把“访问时间”“过期时间”“访问顺序”三者耦合进一个结构,不能拆开维护。
- 别用两个 dict 分别存数据和过期时间——容易不同步,尤其并发下
- 每次
get和set都要更新OrderedDict顺序,同时检查expires_at - 在
__setitem__中做容量检查时,先剔除所有过期项,再按 LRU 裁剪,否则可能保留一堆僵尸过期键占满空间 - 示例关键逻辑:
from collections import OrderedDict import time
class LRUTTLCache: def init(self, maxsize=128, ttl=60): self.maxsize = maxsize self.ttl = ttl self._data = OrderedDict()
def __setitem__(self, key, value): self._prune_expired() # 先清过期 self._data[key] = (value, time.time() + self.ttl) self._data.move_to_end(key) if len(self._data) > self.maxsize: self._data.popitem(last=False) def __getitem__(self, key): if key not in self._data: raise KeyError(key) value, expires_at = self._data[key] if time.time() > expires_at: del self._data[key] raise KeyError(key) self._data.move_to_end(key) # 更新 LRU 顺序 return value def _prune_expired(self): now = time.time() # 从头遍历,删除过期项(注意:不能边遍历边删 dict) to_delete = [k for k, (_, exp) in self._data.items() if exp zuojiankuohaophpcn= now] for k in to_delete: del self._data[k]
Redis 作为替代方案时的取舍
如果你已有 Redis,SET key value EX 60 天然支持 TTL,但缺原生 LRU 淘汰语义——它用的是近似 LRU(或 LFU,取决于配置),且淘汰发生在内存不足时,不是按访问频次主动管理。若业务强依赖精确 LRU 行为(比如热点 key 必须常驻),纯 Redis 不够可靠;这时更适合用本地 ttlcache + Redis 做二级缓存。
另外,redis-py 的 ConnectionPool 复用和序列化开销,在高频小数据场景下,可能比纯内存缓存慢一个数量级。










