无竞态的 cache-aside

主键读取变热后,常见做法是在 PostgreSQL 旁边部署 Redis 或 Valkey,并 使用 cache-aside:先读缓存,未命中时再读 PostgreSQL 并填充缓存,每次 写入后删除对应的键。读取和写入一旦重叠,这种做法就会出问题。读者缓存 未命中后,可能恰好在写者提交前读到该行;写者的 DEL 先到,读者的填充 随后到达,旧行便留在缓存中,直到其他操作将它移除。我们之前介绍过这 种延迟填充竞态。

pg_local_cache 将缓存放进 PostgreSQL 本身,因此缓存及其需要跟随的写入 都处于同一个事务系统中。

如何保持正确性

成本与收益

以下数据来自 3.1.0:两台各配备 16 vCPU 的虚拟机(Xeon Gold 6354,10 GbE), 运行 PostgreSQL 16,并使用 Go/pgx 客户端。基准测试页面 列出所有表格、原始 JSONL 文件和复现步骤。

写入期间的读取。 64 个读者,每次读取一个键,在 60k 行中均匀选键, 30,000 UPDATE/秒:

方案 读取/秒 命中率 过期条目
pg_local_cache 161,919 0.837 0
Valkey cache-aside 42,294 0.577 0
预编译 SQL 106,949 — —

写者数量不受限制时,Valkey cache-aside 在运行结束时留下了过期条目(混合 测试中为 1 个;两个 120 秒探测中分别为 2 个和 1 个)。pg_local_cache 在 每次测试结束时都没有过期条目,包括持续一小时、执行 252M MGET 和 72M UPDATE 的测试。

写入开销。 相比直接写表,通过 pg_local_cache 执行 UPDATE 的吞吐量低 3.6–8.2%;执行 UPDATE 后再向 Valkey 发送 DEL,吞吐量低 28.9–63.7%。

读取吞吐量。 将服务器限制为一对 vCPU 时,pg_local_cache 每秒处理 210k 次读取,单线程 Valkey 为 180k;使用两对时,pg_local_cache 为 337k, 带 I/O 线程的 Valkey 为 266k。使用四对时,两者都达到受客户端限制的约 360k 平台;pg_local_cache 使用 5.2 个服务器 vCPU,Valkey 使用 8.0 个。 单线程 Valkey 每个请求消耗的 CPU 最少。在单键测试中,预编译 SQL 明显落后。

哪些场景不适用

开始使用

pg_local_cache 支持 PostgreSQL 14–18,采用 MIT 许可证。可从 GitHub Releases(deb 和 rpm)、PGXN 或 Pigsty 仓库安装, 然后阅读快速入门。