第 12 / 22 篇

第 11 章:本地 Outbox 解决哪一段原子性

订单提交后需要发布事件。如果先提交订单,再发送消息,进程可能在两步之间退出;如果先发消息,再提交订单,消费者可能看见最终没有成立的业务。Outbox 把订单与待发事件写入同一数据库事务,缩小这段缺口。

11.1 订单与事件共同提交

随书实验一笔事务插入订单和 outbox,提交后两者都存在;另一笔事务插入后回滚,两者都不存在。这个结果证明同一 InnoDB 本地事务的原子性。

START TRANSACTION;
INSERT INTO orders(op_id,qty) VALUES('order-1',1);
INSERT INTO outbox(payload) VALUES('{"operation":"order-1"}');
COMMIT;

它没有证明事件已经进入消息系统,也没有证明消费者只执行一次。发布者、Broker 与消费者的故障窗口仍需分别处理。

11.2 认领与发送之间仍有窗口

多个发布者可以使用队列式锁定读取认领不同记录。本实验用 SKIP LOCKED 验证互不重复的本地选择,以及回滚后其他工作者可以重新选择该行。

若发布者发送成功、更新 sent 标记之前退出,下次可能再次发送。若它先标 sent 再发送,退出时又可能丢失发布。没有额外的跨系统协议,不能由本地表推导全链路恰好一次。

11.3 消费者需要自己的业务身份

事件应携带稳定身份,消费者将去重记录与自己的业务变化共同提交。消费者返回确认之前还是之后发生数据库提交,也有窗口,需要结合实际消息系统的投递和确认语义验证。

这与第 9 章的订单幂等类似,但不能把生产者的一个唯一键直接当作所有下游系统已经实现去重。每个执行副作用的边界都需要自己的合同。

11.4 记录积压与恢复依据

Outbox 应保留事件创建、认领、发送尝试、结果与错误分类。积压量和最老事件年龄可以帮助定位发布停滞;重复发送比例则需要结合消息身份与确认记录。

本书没有连接真实 Broker 的 Outbox 派发器。它完整验证数据库侧共同提交与认领边界,消息投递可结合 RocketMQ 或 Kafka 书中的实验继续实现。生产设计必须再验证发送成功后崩溃、重复投递和消费者提交后确认丢失。

练习。 发布成功后程序退出,outbox 仍为未发送。下一轮可能发生什么?请分别写出生产者记录、事件身份和消费者事务需要保存的信息,并指出哪一项无法只靠 sent 布尔值表示。