配置文件已经改了,lru_cache 为何仍返回旧值:缓存键与失效边界
摘要:给读取配置的函数加上 lru_cache,可以减少重复调用,却不会自动感知文件内容变化。本文用 Python 标准库的本地合成实验,说明缓存键、容量淘汰和主动清空分别影响什么,再给出把缓存移到纯解析步骤的可运行例子。
一个程序首次从 limit.txt 读到数字2,随后文件改成5,再调用同一个函数,结果却仍是2。排查时如果只确认“文件确实保存了”,容易把问题归到路径或编码。真正需要先看的,是这次调用有没有重新执行读取。
路径相同,不代表路径指向的内容相同
本次先定义一个由 @lru_cache(maxsize=2) 装饰的函数,函数体用 Path.read_text() 读取文件,再用 int() 解析。调用参数只有 Path 对象,没有文件内容或版本信息。
缓存首先根据调用参数查找已有结果。命中时,直接返回保存的结果,函数体不会再次执行。因此,文件在磁盘上从2变成5,并不会自动改变这个路径参数对应的缓存键。可哈希的参数只满足建立缓存键的条件,不保证函数结果永远只由参数决定。
本次新建 a.txt 与 b.txt,内容分别为2和7,按固定顺序执行以下操作:
| 操作 | 返回值 | 实际读取文件次数,累计 |
|---|---|---|
| 依次读取 a、b | 2、7 | 2 |
| 将 a 改为5,再读取 a | 2 | 2 |
| 清空缓存,再依次读取 a、b | 5、7 | 4 |
第二步后,另行读取磁盘确认 a 的文本已是 5\n;被缓存的函数却仍返回2。此时 cache_info() 显示 hits=1、misses=2、currsize=2,和函数体只执行两次的计数相符。这是主动构造的本地反例,没有据此宣称某个实际系统存在故障。
maxsize 限制条目数量,不负责过期
maxsize=2 表示最多保留两个缓存条目,没有“保存两秒”的含义。LRU 依据最近使用情况淘汰条目,不依据文件何时被改写判断结果是否有效。
另一组实验依次请求 A、B、A、C、B,缓存函数把大写字母转为小写,同时记录实际执行。结果只执行了 A、B、C、B 四次:第二次访问 A 命中,使 B 成为较久未用的条目;加入 C 后 B 被淘汰,最后访问 B 就需要重算。最终 hits=1、misses=4、currsize=2。
这组结果说明容量淘汰的顺序,不能转写成“旧数据会很快自动刷新”。一个持续被访问的旧结果,仍可能不断命中。若业务需要有效期,应另行定义计时、到期后的加载和失败处理规则;本文没有实现或验证带有效期的缓存。
主动清空也有明确范围
第一组调用 read_limit.cache_clear() 后,hits、misses 和 currsize 都变为0。随后 a、b 均重新读取,说明清空影响的是这个包装函数保存的全部条目,并非只让发生变化的 a 失效。
对于由本进程统一管理的配置更新,更新后主动清空可以是一种选择,但需要明确由谁负责调用。该方法不会通知其他进程的独立缓存;也不能把一次清空解释为多个线程、多个文件更新时的统一版本切换。本次实验全部顺序执行,没有验证这些并发情况。
把变化的读取留在缓存外面
如果想复用的是同一份文本的解析结果,可以每次读取文件,只缓存文本到值的转换。下面的完整例子只处理自行生成的小型整数文本,返回不可变的 int:
from functools import lru_cache
from pathlib import Path
from tempfile import TemporaryDirectory
@lru_cache(maxsize=2)
def parse_limit(text):
return int(text)
def load_limit(path):
return parse_limit(Path(path).read_text(encoding="utf-8"))
with TemporaryDirectory() as folder:
path = Path(folder) / "limit.txt"
for text in ("2\n", "5\n", "2\n"):
path.write_text(text, encoding="utf-8")
print(load_limit(path))
print(parse_limit.cache_info())
本次输出依次为2、5、2,最后一行为 CacheInfo(hits=1, misses=2, maxsize=2, currsize=2)。独立计数的同结构实验确认:读取文件三次,解析函数执行两次。第三份文本与第一份相同,因此可以复用已有的解析结果。
这里改变的是缓存位置。读取函数仍然每次访问磁盘;解析函数的输入则包含本次实际读到的文本,相同文本得到相同整数,不再暗中依赖某个路径当时的内容。如果解析还依赖语言、规则版本等条件,也必须把这些依赖纳入设计,不能只搬动装饰器就假定已经解决。
缓存原始文本也会占用空间,两个条目不等于两个字节。面对较大或外部来源的文件,仍需另外限制读取大小、校验数值范围及处理读取异常。本例只用极短文本解释行为,没有测速度;对一次 int 转换,缓存本身的管理成本可能并不划算,不能由“少解析一次”推导出性能提升。
每次读取也只代表取得了这一次读取返回的内容。它没有让读写文件成为一个原子事务,更没有保证配置发布期间总能读到完整的新版本。缓存失效和文件更新的一致性,应分别定义和验证。
2026年11月7日,本实验在 Windows、Python 3.12.14 下实际运行三组观察,另执行文中完整代码。两个子进程各运行一次,均退出0,stderr均为空,原始 stdout、stderr 字节、调用计数、退出码及 SHA256 清单已本地保存。全部输入是新建的合成文本,未调用网络、云服务或生产系统,也未测并发与性能。判断缓存是否合适,先写清哪些输入决定结果,再决定何时允许复用。
- 点赞
- 收藏
- 关注作者
评论(0)