失败处理与业务一致性:补偿、对账与回放
回答“订单、审批、支付、账务、消息和同步出错后,系统如何发现差异、补正状态并可控重放”
阅读边界: 这页不是单讲分布式事务模式,也不是只讲财务对账;它把幂等、补偿事务、对账任务、消息重试、事件回放和人工修复放成一套业务一致性恢复能力。
一句话判断: 如果系统只设计了成功路径,没有设计失败后的对账、补偿和回放,它就不是真正可运营的跨系统业务。
| 不解决什么 | 真正解决什么 | 适合出现在哪里 |
| 不替代系统边界设计 | 边界已经存在后,处理跨系统动作失败、重复、延迟和部分成功。 | 接口、事件、数据和权限设计之后。 |
| 不等同于强一致事务 | 在无法用单个事务包住的业务链路里,让状态最终可发现、可解释、可修复。 | 支付、审批、订单、账务、物流、消息、数据同步。 |
| 不只是运维补救 | 提前设计幂等、对账、补偿、回放和人工接管,避免事故时临时脚本救火。 | 架构设计和上线评审阶段就要检查。 |
失败可被发现
操作可幂等
差异可对账
状态可补偿
事件可回放
影响可审计
| 场景 | 风险 | 治理判断 |
| 支付 / 账务 | 外部支付成功但内部订单失败,或账务分录缺失 | 支付结果、业务状态和账务凭证必须可对账、可追补 |
| 消息重试 | 重复消费、乱序消费或消息永久堆积 | 先设计幂等键、去重表和死信处理,再谈重试次数 |
| 异步任务 | 任务部分成功,状态停在中间态 | 中间态要有超时扫描、补偿动作和人工接管入口 |
| 事件回放 | 回放触发二次副作用或污染当前状态 | 回放必须区分历史重建、补发通知和真实业务动作 |
幂等设计
幂等不是“多试几次没事”,而是每类外部动作都有业务幂等键、状态机和去重记录。
补偿事务
补偿不是简单反向 SQL,而是按业务语义定义撤销、冲正、补发、关闭或人工处理。
对账任务
对账要有权威来源、差异分类、容忍窗口、处理优先级和闭环状态。
死信与隔离
无法自动处理的消息要隔离、告警、可查询、可修复,而不是无限重试拖垮系统。
事件回放
回放前要定义范围、版本、幂等策略、速率限制和副作用开关。
人工运营入口
高风险修复需要审批、dry-run、审计和回滚方案,不能靠临时脚本隐身操作。
| 失败类型 | 表面现象 | 优先使用的恢复能力 |
| 外部成功、内部失败 | 支付、物流、短信等外部动作已成功,但本地状态未推进 | 对账发现差异,按业务语义补偿推进本地状态或发起反向动作 |
| 内部成功、外部未知 | 本地已提交,但外部调用超时,无法确认真实结果 | 查询外部权威结果,避免盲目重试造成重复扣款或重复发货 |
| 消息重复、乱序或丢失 | 消费者多次收到同一事件,或先收到后置事件 | 幂等键、状态前置条件、去重表、死信队列和回放补齐 |
| 历史逻辑错误 | 一段时间内事件处理逻辑有 bug,当前状态或报表存在系统性偏差 | 冻结范围,修正逻辑版本,使用事件回放或批量补算恢复 |
| 人工修复引入新差异 | 临时 SQL 修了一个系统,却让账务、缓存或下游报表继续不一致 | 把人工修复纳入对账闭环,并要求复核所有受影响系统 |
| 能力 | 它负责什么 | 不能替代什么 |
| 幂等 | 保证重复请求、重复消息、重复执行不会造成二次副作用 | 不能发现漏处理和跨系统差异,所以还需要对账 |
| 补偿 | 在部分成功或状态卡住时,用业务语义推进、撤销、冲正或关闭 | 不能简单等同于反向 SQL,尤其账务和外部动作必须按业务规则处理 |
| 对账 | 周期性比较权威源、业务状态、账务凭证和外部结果,发现差异 | 不能只产出报表,必须连接自动补偿或人工处理队列 |
| 回放 | 用历史事件或任务重新构造缺失状态、修复处理漏洞或重建读模型 | 不能直接走有外部副作用的生产路径,必须隔离通知、扣款、发货等动作 |
| 人工修复 | 处理自动规则无法覆盖的异常样本和高风险个案 | 不能绕过审批、dry-run、审计和修复后对账 |
订单链路
先设计订单状态机和超时扫描,再处理支付结果、库存锁定、履约通知和取消补偿,避免状态停在“处理中”。
账务链路
以账务流水和凭证为权威线索,使用冲正、补记、红蓝字或调整分录,不用普通状态更新代替账务语义。
消息链路
生产端有 outbox 或事件表,消费端有幂等键、去重窗口、死信入口和重放批次,避免“消息发了就算成功”。
迁移链路
迁移期间要并行跑新旧结果对账,出现差异先分类再补偿,不要靠一次性切流赌正确性。
| 误区 | 为什么危险 | 修正方式 |
| 把 MQ 当成可靠性答案 | 消息投递成功不代表业务处理成功 | 对业务状态、外部结果和消费结果分别建账 |
| 用重试掩盖幂等缺失 | 重复扣款、重复发货、重复通知都会被放大 | 所有可重试动作先有幂等键和状态判断 |
| 回放时直接跑生产逻辑 | 历史事件可能触发邮件、扣款、库存等真实副作用 | 回放路径要有副作用隔离和版本适配 |
| 检查项 | 最低要求 |
| 幂等键 | 关键写动作、外部调用、消息消费都有幂等键和去重窗口 |
| 状态机 | 中间态、终态、失败态、可补偿态和人工介入态清晰 |
| 对账口径 | 清楚权威来源、对账频率、差异分类和处理 SLA |
| 回放保护 | 回放范围、速率、版本、幂等和副作用开关明确 |
| 审计闭环 | 补偿和人工修复有审批、执行记录、影响对象和复核结果 |
| 补偿登记字段 | 要写清楚什么 | 缺失后的风险 |
| 补偿对象 | 业务单号、外部结果、内部状态、账务凭证、消息 ID 和 owner,明确这次补的是哪一笔、哪一段链路 | 只知道“有异常”,不知道应该推进订单、补账、补消息还是交给人工处理 |
| 触发条件 | 超时、对账差异、死信、状态卡住、人工确认等触发来源,以及自动触发还是人工审批触发 | 补偿规则变成隐形经验,事故后无法解释为什么某批对象被处理 |
| 业务语义 | 推进、撤销、冲正、补发、关闭、人工处理等动作含义,不能只写反向 SQL 或状态覆盖 | 技术动作看似成功,业务语义却错了,例如该退款的单被直接标完成 |
| 幂等与副作用 | 幂等键、去重窗口、外部动作是否会重复,以及通知、扣款、发货等副作用如何隔离 | 补偿本身制造二次扣款、二次发货、重复通知或重复账务分录 |
| 审计与复核 | 执行记录、影响对象、审批人、对账结果、业务确认和后续 owner | 修完后无法证明影响范围,也无法证明差异已经闭环 |
| 回放门禁 | 必须满足什么 | 失败时动作 |
| 范围冻结 | 事件范围、时间窗、处理版本、目标对象和非目标对象都被明确冻结 | 暂停回放,先产出对象清单和排除清单,避免误伤正常数据 |
| 副作用隔离 | 通知、扣款、发货、外部调用不能被真实触发,或必须切到 mock / dry-run / outbox 隔离路径 | 立即停止批次,检查外部副作用是否已经发生,并进入事故处理 |
| 速率与水位 | 限速、队列水位、下游承载、暂停条件和回退批次策略清楚 | 降速或暂停,先保护下游稳定性,再继续处理差异 |
| 结果校验 | 差异清单、对账口径、抽样验证、回放前后指标和异常样本留痕 | 不得扩大批次,先解释差异变化和新异常来源 |
| 退出条件 | 差异归零,或剩余差异都有 owner、处理队列、复核日期和业务接受结论 | 不能宣告完成,只能标记为待人工闭环或继续补偿 |
| 场景 | 最小落地包 | 验收信号 |
| 支付成功但订单未完成 | 建立支付结果表、订单状态对账任务、补偿动作和人工介入队列;补偿必须按业务语义推进状态,不直接改终态 | 每笔外部成功支付都能落到完成、退款、关闭或人工处理之一 |
| 消息重复或乱序消费 | 为消费动作定义业务幂等键、去重窗口、状态前置条件和死信处理入口 | 重复消息不会产生二次扣款、二次发货或二次通知 |
| 历史事件需要回放 | 先冻结回放范围、事件版本、副作用开关、限速和结果校验,再分批重放 | 回放能修复缺口,不触发真实外部副作用,差异清单逐步归零 |