Appearance
数据库提交和外部副作用如何衔接:Event Log、Outbox 与 Side-effect Consistency
前置:Transaction;Runtime Facts Persistence;Idempotency
核心问题:数据库状态已经提交但消息还没发出去时怎样避免“状态成功、事件丢失”,Outbox 为什么能缩小这个 crash window?
RUN CREATED、NODE STARTED、TOOL SUCCEEDED 依次追加,current state 可由 reducer 构建。Node 完成事务同时插 outbox node.completed ,publisher 之后送 MQ,即使进程中间 crash 也能重试。
Durable Event Log
append-only event log 有利于审计和重建;恢复时从 checkpoint + subsequent events 重放,但外部动作需依据 execution record 避免重复。
Event Log 追加状态变化事实,可用于审计、重建、异步消费;需要稳定 event id、ordering scope 和 schema version。不要随意修改历史事件。
Outbox Pattern 基础
Outbox 解决的是“数据库已经提交,但通知/消息还没发出去”这个 crash window。
text
DB transaction:
update run state
insert outbox event
COMMIT
separate dispatcher:
read outbox
publish message
mark sent / retry idempotently因为业务状态和 outbox row 在同一个 transaction 里,要么都存在,要么都不存在。这样进程在 commit 后立刻 crash,dispatcher 之后仍能补发。它不等于 Exactly-Once,消费者依然要能处理重复投递。
External Side-effect Consistency
External Side-effect Consistency 关注数据库事实与邮件、支付、HTTP API 等无法纳入本地 transaction 的副作用如何最终对齐,包括 idempotency、outbox、reconciliation 和 compensation;它是目标问题,不等于某一种 pattern。
Outbox 把哪个 crash window 封住了
假设 Node 完成后既要更新数据库,又要发布 node.completed 到 Queue。危险写法是:
text
UPDATE node → COMMIT
↓ crash here
publish message数据库已经认为完成,但消费者永远收不到事件。反过来先 publish 再 commit,则可能消息已经消费、数据库却回滚。
Outbox 把“业务状态”和“待发送事件”放进同一个本地 transaction:
sql
BEGIN;
UPDATE node_executions SET status='SUCCEEDED' WHERE ...;
INSERT INTO outbox(event_id, topic, payload, status)
VALUES (:event_id, 'node.completed', :payload, 'PENDING');
COMMIT;独立 dispatcher 之后读取 PENDING row 并投递。即使 publish 成功但 ACK 前 dispatcher crash,也允许再次投递,因此消费者仍需按 event_id 幂等。
于是 Outbox 提供的不是神奇的 cross-system exactly-once,而是一个 durable bridge:本地提交后“这条消息必须最终被尝试发送”这件事不会随着进程死亡而丢失。
Failure Modes
DB Commit 与外部 Side Effect 没有 Durable Bridge
症状: 写 DB 后直接发消息/API,进程在两步间 crash。
为什么危险: DB 已提交但事件没发,或外部成功本地不知道。
处理: 同事务写业务状态+outbox;dispatcher 幂等投递并记录确认。
如何验证
| Case | 断言 |
|---|---|
| DB Commit 与外部 Side Effect 没有 Durable Bridge | commit/dispatch/ack 各点注入 crash 后最终可对账且不重复。 |
自测
- DB 已提交“要发消息”,进程在真正 publish 前 crash。Outbox 为什么能让这两个动作不再依赖一个不存在的跨系统事务?
- 反过来如果外部副作用先成功、DB 后失败,Event Log/Outbox 单独能完全解决吗?还需要什么 identity 或 reconciliation?