17370845950

如何实现一个简单带 LRU + TTL 的内存缓存
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

  • 注意:ttlcachemaxsize 是硬上限,满时按 LRU 清理最久未用项,**不管是否已过期*

    *;过期检查只在 __getitem__get 时触发,不是后台定时扫描

自己实现要注意三个关键点

如果必须手写(比如嵌入无 pip 环境、或需定制淘汰逻辑),核心是把“访问时间”“过期时间”“访问顺序”三者耦合进一个结构,不能拆开维护。

  • 别用两个 dict 分别存数据和过期时间——容易不同步,尤其并发下
  • 每次 getset 都要更新 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-pyConnectionPool 复用和序列化开销,在高频小数据场景下,可能比纯内存缓存慢一个数量级。