functools.lru_cache不支持TTL,因其仅按访问频次和容量淘汰,不记录时间戳,无法在获取时校验过期;需自行封装如TTLCache类,在get中检查time.time()-timestamp>ttl并配合LRU管理。

为什么不能直接用 functools.lru_cache 加 TTL?
functools.lru_cache 本身不支持时间过期,它只按访问频次和容量淘汰。哪怕缓存项已“过期”,只要没被新调用挤出,就会一直返回旧值。你看到的“数据没更新”,往往就是这个原因。
常见错误现象:lru_cache(maxsize=128) 装了某个接口返回值,但该接口后端数据每 5 分钟变一次——缓存却可能几小时都不刷新。
- 它没有存储创建/最后访问时间戳
- 无法在
__getitem__阶段做 TTL 检查 - 装饰器层面无法插入自定义过期逻辑
推荐方案:用 cached_property + 手动时间戳,还是用 LRUCache 自实现?
如果你只是给类属性加带 TTL 的缓存,cached_property 改造最轻量;但通用函数级缓存,必须自己管理键、值、时间戳和淘汰逻辑——别碰第三方包(如 cacheout 或 redis),除非你真需要分布式或持久化。
核心思路是:把 (key, value, timestamp) 封装进一个有序结构,每次 get 时检查 time.time() - timestamp > ttl,过期就删并返回 miss。
- 用
dict存键值对,用collections.OrderedDict维护访问顺序(便于 LRU 淘汰) - 每次
get先 pop 再 re-insert,保证最近访问排在末尾 - TTL 检查必须在 get 里做,不能只在 set 时记时间——否则 get 到的是过期脏数据
- 避免用
threading.Lock:单线程够用;多线程场景下,用threading.RLock更安全
一个可直接粘贴的最小可行实现
下面这段代码跑通了并发读、TTL 判定、容量限制三件事,不到 40 行:
快速生成专业的 Python 脚本和应用代码。一键创建完整项目结构,支持CLI、API、爬虫、Bot、Django等多种项目类型,包含完整的项目结构、配置文件、依赖管理、测试、README和文档。
from collections import OrderedDict
import time
<p>class TTLCache:
def <strong>init</strong>(self, maxsize=128, ttl=300):
self.cache = OrderedDict()
self.maxsize = maxsize
self.ttl = ttl # seconds</p><pre class="brush:php;toolbar:false;">def get(self, key, default=None):
if key not in self.cache:
return default
value, timestamp = self.cache[key]
if time.time() - timestamp > self.ttl:
del self.cache[key]
return default
self.cache.move_to_end(key) # refresh LRU order
return value
def set(self, key, value):
if self.maxsize == 0:
return
if len(self.cache) >= self.maxsize and key not in self.cache:
self.cache.popitem(last=False) # pop oldest
self.cache[key] = (value, time.time())
注意两个关键点:move_to_end 必须在 get 成功后调用,否则 LRU 顺序错乱;popitem(last=False) 是 FIFO 弹出,不是按 TTL 弹——TTL 只管“是否可用”,LRU 管“谁该被淘汰”。
容易被忽略的边界问题
很多人写完发现缓存命中率低、或频繁重建,其实是栽在这些地方:
-
time.time()在容器内多次调用可能有微秒级漂移,但对秒级 TTL 影响不大;若需更高精度,统一用time.monotonic() - key 是可变类型(如
list、dict)会直接报TypeError: unhashable type——务必确保 key 可哈希 - set 时如果 key 已存在,当前实现会覆盖并更新时间戳;但有些场景需要“只设一次”,得加
if key not in self.cache:判断 - 高并发下
get+set非原子,可能重复计算;真要强一致性,得上RLock,但会拖慢 15%~20%
真正难的不是写出来,而是想清楚:你的“过期”到底指“创建后过期”,还是“最后访问后过期”——前者用单时间戳,后者得维护 access_time 字段,且每次 get 都要更新它。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










