functools.lru_cache不支持TTL机制,因其设计为纯LRU淘汰且无过期时间参数;需手写TTLCache类,用OrderedDict存(value, expire_time)并检查时间戳,注意LRU顺序更新、线程安全及精度权衡。

为什么 functools.lru_cache 不能直接加过期时间
functools.lru_cache 是 Python 内置的 LRU 缓存,但它不支持 TTL(Time-To-Live)机制。缓存项一旦写入,就永远有效,直到被 LRU 淘汰或手动清除。你无法通过参数设置“5 秒后自动失效”。强行在函数内检查时间戳,会破坏装饰器的纯封装性,也容易漏掉并发访问下的竞态问题。
手写带 TTL 的 LRU 缓存:用 OrderedDict + 时间戳
核心思路是:用 collections.OrderedDict 维护访问顺序,每个缓存值存储为 (value, expire_time) 元组;每次 get 前检查 expire_time 是否已过期,过期则删除并返回未命中。
关键实操点:
- 用
time.time()(非time.monotonic())便于调试,但注意系统时间回拨会导致误删;生产环境可换用time.monotonic()+ 初始偏移 -
OrderedDict.move_to_end(key)必须在每次get成功后调用,否则 LRU 顺序错乱 - 写入时若已存在 key,需先
pop再重插,避免残留旧过期时间 - 缓存大小限制(
maxsize)和 TTL 要正交处理:淘汰只看数量,过期只看时间
示例片段(简化版):
立即学习“Python免费学习笔记(深入)”;
from collections import OrderedDict import timeclass TTLCache: def init(self, maxsize=128, ttl=60): self.cache = OrderedDict() self.maxsize = maxsize self.ttl = ttl
def get(self, key): if key not in self.cache: return None value, expire_at = self.cache[key] if time.time() > expire_at: self.cache.pop(key) return None self.cache.move_to_end(key) # 更新 LRU 顺序 return value def put(self, key, value): if self.maxsize == 0: return if key in self.cache: self.cache.pop(key) elif len(self.cache) >= self.maxsize > 0: self.cache.popitem(last=False) # 弹出最久未用 self.cache[key] = (value, time.time() + self.ttl)用装饰器包装成类似
lru_cache的用法要复刻
@lru_cache(ttl=30)的体验,需支持带参装饰器、绑定到函数对象的独立缓存实例,并兼容cache_clear()等方法。注意点:
- 装饰器工厂函数必须返回真正的装饰器,不能直接返回缓存实例
- 每个被装饰函数应持有自己的
TTLCache实例,避免跨函数污染 - 把
cache_clear、cache_info等方法挂到 wrapper 上,否则用户调用func.cache_clear()会报错 - 不支持
typed=True(即不同类型的相同值视为不同 key),除非手动序列化类型信息
线程安全与实际部署的坑
上面的 TTLCache 类默认不是线程安全的:get 和 put 中的多步操作(查、删、改、move)可能被并发打断。简单加 threading.Lock 会严重拖慢性能,尤其读多写少场景。
更实用的做法:
- 读操作(
get)不加锁,允许短暂返回过期值(业务能容忍几毫秒偏差) - 写操作(
put)加锁,保证pop和set原子性 - 如果必须强一致性,用
threading.RLock并把整个get流程锁住——但请确认你的 QPS 是否真的需要 - 进程间不共享缓存;多进程部署时,每个进程有独立缓存副本,TTL 各自计算
真正难的不是实现,而是判断“过期”是否必须精确到毫秒级,以及能否接受缓存雪崩时的瞬时穿透。这些权衡点,比代码本身更影响最终效果。










