你的审计日志很可能在骗你:Postgres 18 用一条语句修复它
来源:dev.to — 2026-09-06
📋 概述
作者指出经典的 read-then-write 记账模式(SELECT 余额→应用层算数→UPDATE 写回→INSERT 审计行)存在致命竞态:十个并发 worker 各扣 10 元,结果十人都读到 100、都写回 90,最终余额 90,而审计日志十行整齐一致地各自声称「自己做了那笔 100→90 的扣款」——日志与自己完全一致,没有任何校验器能发现异常,这正是审计日志被用来抓这类问题却反而把它埋掉的原因。Postgres 18 的新特性让 RETURNING 直接支持 old/new 别名,可在同一条语句里既完成原子扣减又读出真实被覆盖的旧值,一条语句同时修复了丢失更新与审计失实两个问题。
🔑 核心要点
- 经典竞态重现:十个 worker 同时各扣 10 元,全部读到 100 写回 90,最终余额 90,审计日志却出现十行内部一致、无空无断的 100→90 记录——日志在给错误背书
- 去掉人为延时后不再是确定性复现但仍高频发生:连续三次运行分别丢失 4、5、6 次写入,几乎跟掷硬币差不多,无需调试器加运气就能踩中
- 真正修复丢失更新的关键并不是新语法而是把算数放进语句里(balance - 10),让读与写同时发生不留缝隙——这点任何版本都能做到
- Postgres 18 的增量是能读到同一条 UPDATE 真正覆盖前的值:RETURNING old.balance AS was, new.balance AS now,让审计行由执行工作的那条语句直接写出,十个并发后余额为 0 且阶梯完整 100→90→…→0
- 附带福利:INSERT 上 old 必为 null,因此 old.id IS NULL 能干净判断 upsert 走了 insert 还是 update,取代了旧代码里 xmax 那套让外行看着像 bug 的魔法
💡 金句
一个读后写模式我写了多年,还加了审计表去抓这类模式制造的问题,却从没注意到审计表自己也继承了同一个竞态、因而根本看不见它——十行记录,完美自洽,却全部是错的。
👍 0
👎 0
← 返回 dev.to 首页