TT Lab
开始
学习 学习路径 课程

不可撤销的变更

批准之后有人先改了数据

在 TT Lab 中继续学习

一句话总结

把批准的版本放进条件里一次性修改,批次中只要有一笔对不上,就整体回滚。丢失了完成响应时,通过同一个变更 ID 的回执来确认事实。

为什么需要它

在取消两笔订单的作业中,第一行成功了,而第二行被其他负责人抢先改掉了。只看函数抛出了错误,就判断什么都没有改变,是错的。如果每个 UPDATE 都是自动提交,第一笔取消已经留下来了。客户批准的是两笔全部取消,而实际上只改了一半。捕获异常的代码和业务变更的原子性,是两种不同的机制。

另一个问题是响应。在数据库中确认了变更和审计之后,如果执行程序终止,调用者就收不到成功。再次执行同样的作业时,只看当前状态,会因为不再是 pending 而发生冲突。要区分这究竟是原来执行的成功,还是其他作业者的变更,就必须持久化记录所应用的批准内容和变更 ID。需要的是在没有完成响应时可以用来确认的回执。

工作原理

取消一行时,UPDATE 的 WHERE 要同时比较 id、tenant、revision、qty 和 pending。匹配就把 state 改为 cancelled,把 revision 加 1,并用 RETURNING 接收被修改的行。没有返回行,说明批准前提已经消失,所以是 Conflict。如果先用 SELECT 比较、再执行没有条件的 UPDATE,两条语句之间可能有其他写入插进来,所以要把比较本身放进修改语句里。

在 PostgreSQL 默认的 Read Committed 下,如果另一个事务正在修改同一行,就可能需要等待。前面的事务提交之后,会把条件对着已更新的行重新核对。检查器会真实地制造这种情形。让另一个连接提高 revision 并持有锁,在此期间开始取消请求,在 pg_stat_activity 中观测到 Lock 等待之后提交。后面的请求必须无法用过期的 revision 取消当前行。这是区别于只是连续调用两个函数的测试的证据。

多个对象需要一个外层事务。要同时设计 cancel_one 独立使用时的保护和 cancel_many 整体的保护。psycopg 的 transaction 上下文进入已经打开的事务之内时会使用 savepoint,所以可以组合成:一行函数不会提前提交外层批次。后面的行出现异常时,传递到外层上下文,前面的行也一并回滚。吞掉异常、继续处理下一行的实现,不符合这个整体应用的契约。

为回执准备的 changes 里放入 change_id、tenant 和规范化的 targets。逐行的 audit 里留下变更前后的 revision 和当时的数量。回执插入、整体变更、审计行全都在同一个事务里。中途出错时,三者都必须不存在;提交之后终止,三者都必须存在。另外写审计文件、事后再去核对的方式,不属于本次的原子变更记录。

如果已经存在相同 change_id 和相同批准内容,就不再重新修改,而返回 False。用同一个 ID 提交不同客户或不同对象,就是 Conflict。如果把重复键无条件当作成功,被错误复用的变更编号就藏起来了。把同一请求的重试与不同请求的冲突区分开,正是回执的作用。即使同时有同一个变更进来两次,也只有一个会真正应用,另一方要确认已有的回执。

在现场相遇的样子

有回执,意味着当时确认了变更,并不意味着现在仍是那个状态。其他负责人之后可能改了数量或删除了行。所以 inspect_change 读取当时的批准内容,reconcile 则另外核对当前的行。按 ID 分为一致、之后变更、缺失,不用数量相同的另一行去填补缺失。也不加入仅凭核对就撤销他人变更的功能。

例如,取消时 revision=4 的行,现在 revision=5,那么即使数量和状态相同,也归为之后变更。即使新出现了另一个 ID 的 cancelled 行,使总数不变,只要原来的对象消失了,就是缺失。只用一个数字来汇报,这两种情况都可能看起来正常。对客户要把当时的确认记录和当前的核对结果并排说明,不能仅仅因为有差异,就随意编造原因。

实验数据库的设置与生产的耐久性标准不同。基础镜像中的 fsync 和 full_page_writes 是关闭的,所以本次测试只验证连接着正常运行的 PostgreSQL 服务器的客户端被终止的情形。不要把“提交前连接断开会回滚,提交后重新连接能找到回执”这一观测,扩大成服务器断电或磁盘损坏恢复的保证。带到真实服务上时,另需验证生产存储、备份和恢复。

下一项实验要做什么

从批准计划校验开始,分 8 步实现单行和批次变更、审计回执、当前状态核对以及连接生命周期。最后真实地在提交前后终止进程,并用同一个变更 ID 重新尝试。不只看成功的请求,还要确认其他客户、过期版本、重复 ID、部分提交,以及把当前状态与回执混为一谈的错误答案。

参考:PostgreSQL 隔离级别、psycopg 事务管理。数量和版本范围、回执 schema 和异常分类,都是本实验的契约。