读穿透缓存:未命中时取数回填
读穿透缓存给调用者提供统一的读取接口:命中时返回已存值,未命中时调用加载函数,再把结果保存起来。关键边界是由谁拥有这个 loader。把它放到缓存接口后面,可以减少重复的查找逻辑,但并不会自动解决并发、写入、访问授权或不同缓存副本之间的一致性。模式名称不是这些能力已经具备的证明。 找到真正负责加载的一层 在 cache-aside 模式中,应用显式查询缓存,未命中后访问数据源并回填;读穿透则让缓存组件拥有这条未命中路径。它可以是应用进程里的库,也可以是远程缓存服务。两种部署方式各有更新边界:修改进程内库的 loader,仍然需要把变更部署到使用它的进程,不能笼统宣称所有读者都会免部署地自动获得新逻辑。 单独一个 Redis GET 也不是读穿透加载器。字符串键不存在时,它返回 nil,不会自动发现并查询你的数据库。客户端库、缓存服务或应用适配层必须另外提供加载逻辑。下面的实验用带记忆功能的函数包装器提供统一读取接口,缓存和数据源都在当前进程内;这不是 Redis 服务端加载功能的演示。 读取模式没有定义写入模式 读穿透本身没有规定写操作经过哪里,写穿透是另一项安排。即便读写共用一个组件,仍需要定义数据源写入和缓存更新分别失败时的行为。绕过组件的其他写入方,可能让缓存保持旧值。应沿真实业务更新路径检查参与者,不能因为使用了一个模式名称,就假定所有写入都会配合清理。 TTL 限制的是某个缓存条目的复用时间,并不保证下一次重载从滞后副本或有缺陷的 loader 中取到最新真值。过期也不一定立即从内存中释放字节。寿命应依据数据需求选择,而非套用统一的几秒或几分钟;用户数据的 TTL 再短,也不构成权限隔离。命中率很高同样不能证明内容正确。 坏掉的样子 记录不存在与数据源不可用,是两个不同结果。缓存一次合法的“不存在”可以减少重复查询,但如果随后创建了记录,这个否定结果也可能继续遮住新记录,直到到期或失效。若把超时转换成一次正常返回的空值,就可能把故障写成错误的缓存内容。必须明确哪些结果允许保存,以及调用者如何区分缺失和失败。 并发加载是另一项独立契约。Cachetools 明确说明:锁保护缓存对象的访问,但被包装的函数在锁外运行,因此只有锁并不能抑制重复加载。它的 condition 选项另行处理同键的并发执行。下面的实验只运行单线程,刻意不使用这两个参数,不能把代码当成已经具备线程安全或分布式协作的实现。 失效还可能与一个已经读到旧源版本的加载操作竞争。删除某个键,并不天然阻止那个在途结果稍后重新写入。应根据真实的写入和加载协议选择协调方式,并测试操作顺序。也不要把大范围按键模式删除当作默认修复:它会让更多条目变冷,却没有证明源头缺陷已经解决。 快速验证 需要 Python 3.10 或更新版本;本次使用 Python 3.12.14 和 cachetools 7.1.8。安装固定依赖后,用 "$readthrough_lab/venv/bin/python" 执行 Python 代码。缓存存储、到期和函数记忆行为都由现有库负责;测试手动推进专用时钟,无需睡眠,也不修改系统时间。实验不联系 Redis 或外部数据库,只有依赖安装需要联网。 ...