Skip to content

方法Method

事务发件箱与外部副作用

Transactional outbox · Outbox pattern · External side-effect delivery

将业务更新与待发消息放在同一提交域,用中继重试和接收去重解释不丢意图、可能重复及外部效果的承诺边界。

形式陈述 ​

事务发件箱把“业务改变”和“应该对外发送什么”作为同一个本地持久事务或确定性复制命令的结果。设业务状态为B,待发送表为O;执行业务请求时原子产生(B',O∪{(m,destination,payload)}),其中消息身份m在重试间不变。若该业务请求本身可重试,也必须有业务请求去重,否则两次真实提交仍会产生两条合法消息。

提交后,中继读取O,向指定接收者发送m,取得接收者合同规定的成功确认后再持久标记已交付。未获确认或崩溃恢复后,中继继续发送未标记项目。复制状态机应用命令时只改变O,不直接发外部请求;否则每个副本重放同一命令都会各自发出副作用。

发件箱的本地安全不变量是:业务事务一旦提交,发送意图也在同一可恢复状态中;事务若未提交,则中继不能看到并发出这份意图。它不保证接收端只执行一次。中继可以发送多次,同一消息可以被网络复制,确认可能丢失。

若接收者也提供原子持久去重,把m、原响应与其业务效果一起提交,才可将重复投递限制为接收端某个明确原子域内的一次效果。此承诺的范围、身份保留期与会话过期条件必须逐项说明。不能从“用了消息队列”或“用了outbox”直接推导端到端无条件exactly-once。

直觉

直接写数据库再发消息,有一个“数据已变,进程还没来得及发”的空档;先发消息再写数据库,则有“外面已经行动,本地却回滚”的相反空档。Outbox把待办内容与业务一起存下,关闭的是发送意图会不会被遗忘的问题。

中继与接收者之间仍是一轮结果可能未知的RPC。接收者做完之后确认丢了,中继就不知道该停止还是再试。记录“已经发过”不是记录“对方已持久完成”;无论把它放在发送前还是发送后,都要面对另一个崩溃窗口。

例子与边界

一次登记,两次通知尝试 ​

订单库初始没有订单,也没有待发记录。一项原子事务写入order7=CONFIRMED和O[m7]=(warehouse,reserve(7)),提交完成后接口只承诺“订单已登记,通知待处理”,不承诺仓库已经预留。

中继发送m7;仓库预留一件商品并提交,库存从5变4,但确认丢失。中继仍看见m7未交付,于是重发。没有接收去重时,库存可再从4变3;有原子Seen[m7]与预留结果时,第二次只返回原结果,库存保持4。随后中继持久标记m7已交付。

这里两次发送、两次接收都允许发生;受保护的是仓库定义的业务效果。若仓库提交后又调用一个没有去重合同的短信网关,短信仍可能重复,保证不会自动越过这个新边界。

标记前后都可能崩溃 ​

若中继先标记“已完成”,随后发送前崩溃,恢复会跳过m7,通知永久丢失。若在仓库提交后、中继标记前崩溃,恢复重发m7,产生重复投递。这说明只靠发送方的一个布尔标记,无法同时关闭两边独立原子域的窗口。

正确outbox选择保留未确认项目并允许重试,再由接收端幂等或去重处理重复。这里的“最终至少投递一次”还依赖中继持续运行、接收者最终可用、通信重试最终成功以及待发记录不被提前删掉;永久网络中断时只有保存意图的安全保证。

顺序也需另行建立 ​

同一订单先提交m7:CONFIRMED再提交m8:CANCELLED,两个中继并发时可能先送达m8再送m7。Outbox的存在不自动给接收端业务顺序。可按订单键串行投递并等前一项确认,或让接收者按业务版本拒绝旧版本并处理缺口;不能只按全局消息到达时间覆盖状态。

接收者回收Seen[m7]后,极迟的重试仍可能再次预留。因此保留期要覆盖允许重放窗口,或在过期后拒绝旧消息/旧会话;“去重表每天清空一次”不能与永久一次效果承诺同时成立。

推论与应用

可以把整个流程分成三个可观察里程碑:源业务与意图已提交;接收端效果已按其合同提交;源端已记录接收确认。它们通常不是一个原子时刻。若API要承诺第二层,就必须拿到或查询第二层的可靠证据;不能把第一层成功响应改名为“外部付款已完成”。

在复制服务中,O和业务结果表一样必须随同前缀快照恢复。新leader可接管中继,旧leader也可能有已发出的请求,因此身份稳定和接收去重仍有用;必要时再用fencing隔离旧中继的其他资源操作。Fencing不会单独识别同一代重复消息。

待发积压为p条、平均payload为c字节时,源端至少保留O(pc)字节,另有索引和结果;接收去重也消耗保留记录。若输入速率长期大于确认速率,p会无界增长,应有背压、配额或明确失败策略。删除已确认记录可以控制空间,但不能以删除为由允许旧身份变成一项新业务。

不可撤销副作用需要更谨慎的承诺:发送邮件、打印、人工处理未必提供可原子验证的业务ID。补偿是另一项后来执行的业务动作,可能失败,也不会让已读邮件“从未发送”。完整服务应公开“已登记/处理中/结果未知/已确认”等实际状态,而不是用复制日志掩盖这些边界。

参考资料
  • Chris Richardson,Transactional outbox:业务与消息同事务、中继重复发送及消费者幂等责任
  • G. Ramalingam and Kapil Vaswani,Fault Tolerance via Idempotence,POPL2013,§§1–3:局部原子域、重复请求与容错组合;本文订单/库存数字和中继事件模型为自定教学例子
关系图谱7 个相邻概念 · 2 类关系

拖动节点调整位置。

显示关系

显示:依赖

  1. 前置三跳
  2. 前置二跳
  3. 前置一跳
  4. 当前条目
  5. 后续一跳
  6. 后续二跳
  7. 后续三跳
文字版关系按与当前条目的最短距离分组
类型化关系