一个账户余额100,十个工人各自取走10。每个工人都做了最正常的事:读余额、减掉10、写回去、记一条日志。最终余额停在90,九笔取款凭空消失。
后端工程师看到这里会说:这是典型的竞态条件,老问题。但真正让人后背发凉的不是丢钱,是那十行审计日志。
![]()
十行记录,每一行都写着余额从100变成90。没有缺口,没有空值,没有孤儿外键。任何模式校验、校验和、夜间对账任务都挑不出毛病。
日志的“自洽”骗过了所有检查
事故发生时拿到这张表,你会怎么读?大概率会判断成“一笔取款被重试了十次”,然后去查重试逻辑。重试逻辑没问题,于是一个下午就耗在那里。
在受监管的支付系统里,审计日志不是调试工具,它就是证据本身。审计员读它,争议处理依据它,监管问询时拿出来的也是它。
所以标准不能只是“看起来合理”,而必须是“这就是事实发生的记录”。而这次实验里的失败,恰好通过了所有针对日志的自动检查。
能自动检查的,全是“自洽”
仔细想想那些自动化检查能问什么:行数对不对、字段全不全、外键有没有断、校验和是否匹配。每一个问题都在问日志是否前后一致,没有一个在问日志是否真实。
十个进程都以为自己是唯一在操作的人,各自写下了自己眼中的“真实世界”。当十个诚实的叙述者拿着同一份错误信息时,你得到的恰恰就是完美的自洽。
要发现这个问题,你需要一行跟另一行互相矛盾的记录。但没有。矛盾才是信号,而这个bug把信号删掉了。
“没有报警”不等于“没有问题”
作者周一发过一篇文章,讲的是没人检查存活状态的护栏:一条停止运行的lint规则、一个什么都匹配不到的策略检查、一个跳过一半语料却只报告跑完那半的评估。
评论区讨论得很长,得出的结论比文章本身更尖锐:一个从不触发的检查和一个从不自相矛盾的日志,给出的读数完全一样——一切正常。
两种情况里,“没有投诉”都被当成了健康的证据,而本该产生投诉的机制,恰恰就是坏掉的那个。输出是干净的,这才是问题所在。
Postgres 18给出的新思路
Postgres 18允许在同一条语句里命名变更前后的行:
UPDATE accounts SET balance = balance - 10 WHERE id = 1 RETURNING old.balance AS was, new.balance AS now;
语法很漂亮,但这不是重点。重点是日志条目现在从写入操作本身派生出来,而不是另写一条来描述前一条写入。此前,日志是第二次写入,描述的是第一次写入;现在,它成了写入的一部分。
当记录和动作绑定在同一个原子操作里,那种“十个诚实叙述者各写各的”的错位空间就被压缩了。日志不再是一份可以独立漂移的副本,而是动作发生时留下的直接痕迹。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.