用 Python 标准库实现带过期时间与 LRU 淘汰的本地缓存
【摘要】 为什么要自己写一个 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)