缓存失效与延迟填充竞态
“数据库更新后删除缓存键”仍留下一个时序问题:另一个请求可能已经在加载旧值。删除移除的是当前存在的条目,不会取消仍在途中的结果。
跟踪两个请求
假设 PostgreSQL 保存 revision 0,应用使用 cache-aside 读取。即使写者在提交之后使缓存失效,也可能发生以下顺序:
| 步骤 | 读者 A | 写者 B |
|---|---|---|
| 1 | 缓存未命中,读取 revision 0 | |
| 2 | 在存储结果前暂停 | 将该行更新为 revision 1 |
| 3 | 提交并删除缓存键 | |
| 4 | 发布先前读取的 revision 0 | |
| 5 | 后续请求读到过期缓存值 |
TTL 可以限制该值保持可用的时间,却无法使第 4 步变得正确。将删除移到提交之前,会产生另一个时间窗口,使读者能够用旧的已提交数据库状态重新填充缓存。PostgreSQL 与 Redis 指南解释应用 cache-aside 与数据库本地行缓存的边界。
不仅检查查找,也检查发布
缓存填充需要证明:结果在发布时仍然符合使用条件。在 pg_local_cache 2.0 中,写入为受影响的键或关系设置屏障;填充携带代次信息,存在记录的缓存项携带元组可见性信息。代次变化可以拒绝旧填充。无法安全使用缓存项的读取会回退到 PostgreSQL。
这些检查属于一条特定的 PostgreSQL 读取路径,不会把可选 RESP 接口变成通用 Redis 服务器,也不会使应用已经复制到其他位置的值失效。
同时测试回滚与提交
在一次性数据库上执行双会话测试。在一个会话预热该行,在另一个会话更新该行但保持事务打开。检查三个现象:
- 写者通过源表路径能够读取自己的改动。
- 写事务仍打开时,另一个会话仍看到已提交的值。
- 回滚后保留原值;提交后,
READ COMMITTED下的新语句看到新 revision。
第三个条件指的是新语句。较早开始的语句,无需在执行途中采用更新的快照。事务约定还说明了哪些模式会绕过缓存。需要行锁时使用普通 SQL。
检查应用中的下一层缓存
请求本地的 DataLoader 可能仍保存修改前加载的值。数据库失效机制无法移除这个 JavaScript 对象。发生修改后,应按照应用授权与结果约定,清除或替换受影响的 loader 条目。将 loader 限定在单个请求范围内。
批处理指南区分了请求记忆化与共享行缓存。排查过期响应时,应从数据库快照到响应对象,追踪每一个存储位置。某个边界上的正确性,不会自动清除其他位置的数据。