Appearance
外部 Tool 不能回滚怎么办:Transactional Boundary 与 Compensation
前置:数据库提交和外部副作用如何衔接:Event Log、Outbox 与 Side-effect Consistency
核心问题:外部 Tool 已产生副作用却无法跟数据库一起回滚时,系统怎样选择幂等、对账或补偿而不是假装事务覆盖了它?
调用 30 秒部署 API 时不持数据库 row lock,避免阻塞其他 worker。已创建云资源但后续步骤失败,可执行 delete 作为补偿;如果 create 响应丢失,先 list/query reconcile 再决定。
Transactional Boundary vs External Tool Call
数据库事务只能原子覆盖本 DB,无法把 HTTP/邮件/支付纳入同一 ACID transaction。应先提交 intent/execution record,再外部调用并记录结果,或用 outbox/saga;不要在 DB lock 内等待慢 Tool。
正确边界通常是短事务记录 intent/claim→提交→外部调用→短事务记录结果,而不是在事务里等待网络。必要时结合 outbox/saga。
Compensation / Reconciliation 基础
数据库事务只能回滚自己控制的数据,不能把已经发生的外部动作一起“撤销”。
例如:
text
1. 创建云资源成功
2. 本地写 success 前进程 crash
3. resume 后只看到“本地没有成功记录”这时不能假设“事务回滚了,所以云资源也不存在”。Runtime 需要用 stable operation_id 去外部系统查询实际状态,这叫 reconciliation。
如果外部动作确实需要撤销,例如预订成功后后续流程失败,再调用 cancel_reservation 是 compensation。它是新的业务动作,不是数据库 rollback,因此也可能失败并需要继续对账。
用“创建云资源”走一遍未知结果
Runtime 准备调用远程 create_vm:
text
1. DB: operation(op-42, status=INTENT) commit
2. HTTP create_vm(operation_id=op-42)
3. remote actually creates vm-9
4. response is lost
5. Runtime timeout此时最危险的动作是直接重试 create_vm。正确策略取决于远端能力:
- 支持 idempotency key:用同一个
op-42retry; - 支持查询:先
get_by_operation_id(op-42)做 reconciliation; - 两者都不支持:把结果标记为
UNKNOWN,可能需要人工对账。
如果后续整个工作流失败,delete_vm(vm-9) 可以成为 compensation。但 compensation 也可能失败、也可能不是完全逆操作,例如已经发送的邮件无法“撤回到从未发送”。所以 Saga 的本质不是伪造 ACID,而是显式记录部分成功,并为每个业务动作定义可接受的恢复策略。
Failure Modes
把跨系统 Exactly-Once 当默认保证
症状: 认为事务/队列能让 DB 与第三方 API 原子提交。
为什么危险: 不确定结果无法处理。
处理: 明确 at-least-once/idempotency/reconciliation,必要时 compensation。
如何验证
| Case | 断言 |
|---|---|
| 把跨系统 Exactly-Once 当默认保证 | 第三方成功本地超时后可查询/对账而非盲目 retry。 |
自测
- 付款 Tool 已成功,随后本地 transaction 回滚。为什么数据库 rollback 不能撤销外部世界?
- 对于不可逆副作用,compensation 与“直接 retry 原动作”有什么语义区别?什么时候只能进入人工 reconciliation?