Saga、补偿与中间状态
Saga 把一个长流程拆成多个已经提交的本地事务。后续步骤失败时,系统执行对应的补偿事务修正业务结果;此前变化曾经真实可见,所以补偿不等于把数据库恢复到从未发生过。
1. 每一步都是独立本地事务
订单流程可以拆为:创建订单、预留库存、授权支付、创建履约。每一步在自己的服务内提交,成功后触发下一步。
流程进行中,其他请求可能看到 PENDING_PAYMENT、STOCK_RESERVED 等状态。业务和 API 必须承认这些中间状态,不能只设计“全部成功”和“全部失败”。
2. 失败后按业务语义补偿
库存预留的补偿是释放预留,支付授权的补偿可能是撤销授权;已经完成扣款则可能需要退款。退款是一笔新的交易,可能有手续费、汇率变化和到账延迟。
补偿应让业务不变量重新成立,不要求字节级恢复原值。已经发送的邮件、已读通知和物理发货无法真正撤销,只能发送更正、拦截或人工处理。
3. 编排式 Saga 集中保存流程
Orchestrator 记录当前步骤并向参与者发命令,根据结果决定下一步或补偿。流程图、重试和超时较集中,适合步骤多、分支明确的业务。
协调器不能只把状态放在内存。每次状态转换和待发送命令要持久化,重启后能够从确定位置继续。它是流程控制点,需要高可用和分区容量,但业务数据仍由各参与者拥有。
4. 协同式 Saga 由事件连接
Choreography 让每个服务订阅前一步事件并发布自己的结果,没有单一编排器。简单流程耦合较低,但步骤增多后,很难从一个位置看出全局状态,循环依赖和意外触发也更难排查。
即使使用协同式,也应有稳定 saga ID、统一事件语义和可查询状态投影。否则故障时只能从分散日志猜测流程走到哪里。
5. 补偿操作需要幂等和可重试
消息和响应会重复,协调器无法确认补偿是否完成时会再次调用。参与者按 saga ID 与步骤 ID 保存结果,并用条件状态转换保证 Confirm 或 Compensate 只产生一次业务效果。
补偿遇到瞬时错误可以退避重试;业务上无法补偿时,应进入明确的人工处理状态,而不是无限循环并阻塞整个队列。
6. 并发 Saga 需要隔离策略
两个流程可能同时操作同一库存或账户。Saga 没有跨步骤数据库隔离,可以使用:
- 语义锁或状态标记,表示资源正被某流程占用。
- 版本号和条件更新,拒绝基于旧状态的写入。
- 交换式更新,让补偿成为追加事件而非覆盖当前值。
- 按业务键串行处理冲突命令。
补偿不能无条件把值写回旧快照,否则会覆盖其他流程期间产生的合法变化。
7. 不可逆步骤放在决策点之后
流程中某一步完成后,后续不再尝试全局补偿,而是只向前重试直到完成,这一步常被称为 pivot。发货、正式扣款或不可撤销通知应尽量在前置检查和可补偿预留都成功后执行。
若后续仍可能永久失败,就要准备人工完成、替代履约或客户沟通,而不是声称 Saga 一定能自动回滚。
8. 超时是一种业务结果
调用超时表示结果未知,不能直接当成参与者失败并立即补偿。应使用同一幂等 ID 查询状态,等待事件或由协调器进入 UNKNOWN 并定期核对。
如果原操作实际成功、补偿又同时执行,可能产生新的冲突。参与者状态机要规定每个命令在当前状态下是否合法。
9. 常见问题
9.1 Saga 是否保证强一致
通常不保证跨步骤的隔离和瞬时原子可见。每个本地事务提交后,中间状态可见,系统通过补偿和重试最终达到允许的终态。需要同步强不变量时,应使用本地事务、预留或更强协调。
9.2 补偿失败怎么办
记录独立补偿状态并按错误类型重试,超过上限进入人工队列;同时限制风险继续扩大,例如冻结相关资源。还要有对账发现未进入正常流程的静默失败。
10. 面试题
10.1 Saga 的补偿为什么不等于数据库回滚
出现公司:阿里巴巴
考察重点
- 已提交本地事务和可见中间状态。
- 业务补偿、不可逆动作与并发隔离。
- 编排、协同、超时未知和人工处置。
相关内容:第 1 节“每一步都是独立本地事务”至第 8 节“超时是一种业务结果”。
参考回答
Saga 的每一步都是已经提交的本地事务,结果可能已经被其他请求看到。后续失败时执行的是新的业务补偿,例如释放库存或发起退款,不是数据库把整个系统回到共同快照。邮件、发货和退款成本等外部结果还可能无法完全撤销。
所以流程要显式保存中间状态,补偿按 saga ID 和步骤 ID 幂等,并用版本或条件更新避免覆盖其他并发流程。调用超时先视为结果未知并查询状态,不能立即假设失败。补偿永久失败时进入可告警、可人工处理的终态,不可逆步骤尽量放在所有可补偿检查之后。