PostgreSQL 中的事务感知缓存失效

如果较早开始的读取能在删除后重新填充缓存,仅删除条目是不够的。假设读者开始加载旧行,写者提交新值并使键失效,随后先前的加载器才发布结果。缓存还必须拒绝这种迟到的发布。

2.0 实现在数据库写入路径上,为受影响的键或关系设置屏障。填充携带代次信息,因此失效后可以被拒绝。存在记录的缓存项还带有元组可见性信息。不符合条件的条目会回退读取源表。约定详见技术参考

更新过程中读者看到什么 在 READ COMMITTED 下,从 revision 0 开始。一个事务将其更新为 1。写者通过源表读取看到 1,另一个会话仍看到 0。COMMIT 后,另一个会话的新语句看到 1;ROLLBACK 后,它仍看到 0。 已提交行revision = 0 BEGIN; UPDATE 事务尚未结束 写者读取 1 另一个会话读取 0 COMMIT ROLLBACK 下一次读取revision = 1 下一次读取revision = 0
READ COMMITTED,从 revision 0 开始。“下一次读取”指另一个会话在 COMMIT 或 ROLLBACK 之后执行的新语句。写者通过源表读取自己的改动。

使用两个会话测试

启动本地演示,在两个终端中分别执行:

docker compose -f examples/compose.yaml exec postgres \
  psql -X -v ON_ERROR_STOP=1 -U demo -d pglc_demo

在会话 A 中读取行 42 并记下 revision,再读取一次以预热缓存:

SELECT (local_cache.mget('public.items'::regclass, ARRAY[42]::bigint[]))[1]::jsonb ->> 'revision';
SELECT (local_cache.mget('public.items'::regclass, ARRAY[42]::bigint[]))[1]::jsonb ->> 'revision';

在会话 B 中更新该行,但保持事务打开:

BEGIN;
UPDATE public.items SET revision = revision + 1 WHERE id = 42;
SELECT (local_cache.mget('public.items'::regclass, ARRAY[42]::bigint[]))[1]::jsonb ->> 'revision';

B 能看到自己增加后的值。此读取会绕过缓存。保持 B 事务打开,在 A 中重复查询:A 必须仍看到已提交的 revision,而不是 B 的未提交值。在 B 中执行 ROLLBACK;A 的后续查询仍应返回原始 revision。

现在在 B 中运行:

BEGIN;
UPDATE public.items SET revision = revision + 1 WHERE id = 42;
COMMIT;

A 中在这次提交之后启动的查询,必须返回增加后的 revision。这是相关边界:较早启动且仍在运行的语句,无需切换到启动之后创建的新快照。PostgreSQL 的 Read Committed 文档说明了该行为。

可运行的 Node.js 测试使用独立连接验证这些观察结果。

主动绕过缓存的情况

REPEATABLE READSERIALIZABLE、恢复、并行执行,以及已经写入映射数据的事务,都使用源表路径。超大行可能成功返回,但不会被缓存。命中率接近零不一定代表安装失败:请检查工作负载和绕过计数器。

如果应用需要 SELECT ... FOR UPDATE,应使用普通 PostgreSQL 操作;mget 不能替代行锁。

检查未命中的原因

以管理员身份使用 local_cache.stats()local_cache.health()。比较受控测试前后的计数器快照,并将 SQL mget 与 RESP 计数器分开观察。主动执行 DDL 后,遵循文档中的 reconcile_tablereconcile_all 流程,不要假定旧映射仍然描述修改后的表。

更新于 .