用 Python 标准库实现带过期时间与 LRU 淘汰的本地缓存

举报
yd_238692410 发表于 2026/10/30 09:45:54 2026/10/30
【摘要】 为什么要自己写一个 TTL 缓存在服务里我们经常需要把一些“一段时间内有效”的结果缓存起来,比如鉴权结果、配置、第三方接口返回。直接用 dict 会无限增长,引一个完整缓存组件又偏重。其实用标准库的 OrderedDict 加单调时钟,几十行就能得到一个带过期时间(TTL)和容量上限(LRU 淘汰)的小缓存。设计要点1)容量上限:用 OrderedDict 记录插入/访问顺序,超限时淘汰最久...

为什么要自己写一个 TTL 缓存

在服务里我们经常需要把一些“一段时间内有效”的结果缓存起来,比如鉴权结果、配置、第三方接口返回。直接用 dict 会无限增长,引一个完整缓存组件又偏重。其实用标准库的 OrderedDict 加单调时钟,几十行就能得到一个带过期时间(TTL)和容量上限(LRU 淘汰)的小缓存。

设计要点

1)容量上限:用 OrderedDict 记录插入/访问顺序,超限时淘汰最久未使用的键(LRU)。

2)过期时间:每个键记录一个到期时间点,读取时惰性判断是否过期。

3)时钟选择:用 time.monotonic() 而不是 time.time()。系统时间可能被 NTP 或手动调整,而 monotonic 单调递增,更适合度量时间间隔。

完整实现

"""一个带过期时间(TTL)与容量上限(LRU淘汰)的本地缓存实现,仅用标准库。"""
from collections import OrderedDict
import time


class TTLCache:
    def __init__(self, capacity: int, ttl: float):
        self.capacity = capacity
        self.ttl = ttl
        self._data = OrderedDict()  # key -> (value, expire_at)

    def _now(self):
        return time.monotonic()

    def _expired(self, item) -> bool:
        _, expire_at = item
        return self._now() >= expire_at

    def get(self, key):
        item = self._data.get(key)
        if item is None:
            return None
        if self._expired(item):
            del self._data[key]
            return None
        self._data.move_to_end(key)  # 命中即刷新为最近使用
        return item[0]

    def put(self, key, value):
        if key in self._data:
            self._data.move_to_end(key)
        self._data[key] = (value, self._now() + self.ttl)
        if len(self._data) > self.capacity:
            self._data.popitem(last=False)  # 淘汰最久未使用

    def __len__(self):
        return len(self._data)


if __name__ == "__main__":
    cache = TTLCache(capacity=2, ttl=0.5)

    cache.put("a", 1)
    cache.put("b", 2)
    print("put a,b      ->", "a =", cache.get("a"), " b =", cache.get("b"))

    # 再访问一次 a,使其成为最近使用;此时 b 变成最久未使用
    print("touch a      ->", "a =", cache.get("a"))

    # 容量为 2,插入 c 时淘汰最久未使用的 b,a 因刚被访问而保留
    cache.put("c", 3)
    print("put c        ->", "a =", cache.get("a"), " b =", cache.get("b"), " c =", cache.get("c"))

    # 等待超过 ttl 后,剩余条目全部过期
    time.sleep(0.6)
    print("after 0.6s   ->", "a =", cache.get("a"), " c =", cache.get("c"), " len =", len(cache))

运行一下

容量设为 2、TTL 设为 0.5 秒,演示“命中刷新近期使用”与 LRU 淘汰、以及整体过期:

put a,b      -> a = 1  b = 2
touch a      -> a = 1
put c        -> a = 1  b = None  c = 3
after 0.6s   -> a = None  c = None  len = 0

几个容易踩的坑

- 惰性过期 vs 定时清理:惰性过期实现简单、无后台线程,但过期键会占内存直到被访问或被淘汰。键多且访问稀疏时,可另起线程定期清理。

- 线程安全:上面的实现不是线程安全的。多线程场景需要加锁,或整体用 threading.Lock 包裹读写。

- TTL 精度:过期判断发生在调用 get 的时刻,适合容忍一定延迟的缓存,不适合严格定时淘汰。

小结

用 OrderedDict + time.monotonic 几十行就能得到一个可用的 TTL + LRU 缓存,零第三方依赖。本文代码已在本机 Python 3 环境实际运行,输出与上文一致。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。