Saga 用局部事务和补偿组织长流程

说明长流程怎样拆为可提交步骤,并在后续失败时执行语义补偿、恢复重试和人工处置。

Saga 用局部事务和补偿组织长流程

跨多个服务的业务流程可能持续数分钟、数天,期间还要等待人工或外部机构。若用一个全局数据库事务锁住所有资源直到结束,参与者必须长期在线,阻塞和故障概率都会上升。Saga 把长事务拆为一系列可独立提交的局部事务,并为已完成步骤准备补偿。

补偿不是数据库回滚的远程版本。局部事务提交后,其结果可能已被其他流程看到;补偿只能通过新的业务动作修正影响,不能让历史观察消失。

每个步骤都要有明确提交边界

Saga 的一步应在单一资源边界内原子提交,例如创建订单、预留库存或登记付款请求。步骤成功后,流程状态必须持久化,再决定下一步。只在内存中推进会在进程崩溃后丢失已完成位置。

步骤接口要区分业务拒绝、可重试技术错误和结果未知。超时只说明调用方没有及时收到结果,不证明对方未执行。盲目重试可能重复扣款或重复发货,因此每次业务操作需要稳定幂等键和可查询结果。

局部提交顺序影响可补偿性。不可逆或代价高的动作应尽量后置;可预留再确认的资源,可采用“保留—确认—释放”而不是先永久消耗再补偿。

补偿表达业务上的反向动作

库存预留的补偿可以是释放预留,付款授权的补偿可以是撤销授权,已经结算的付款则可能只能退款。退款和原扣款是两条记录,期间的汇率、手续费和用户可见状态都可能不同。

补偿也可能失败。它需要自己的重试、幂等和告警策略,并且通常按已完成步骤的逆序执行。若补偿依赖的外部系统长期不可用,流程必须进入可识别的待人工处置状态,而不是无限静默重试。

有些动作无法真正补偿,例如邮件已读、实体设备已启动或信息已披露。设计时应明确不可逆点,在此之前增加确认,在此之后采用后续纠正和沟通,而不是声称系统能恢复到从未发生。

隔离弱化会暴露中间状态

Saga 各步骤之间允许其他事务交错,因此不具备单数据库事务那样的隔离。其他流程可能读到暂时预留的库存或处理中订单,并据此作出决定。

可以通过语义锁、状态机、版本检查和可撤销资源降低异常。例如订单处于 PAYMENT_PENDING 时禁止发货,库存记录区分可用与预留。状态含义应进入查询契约,而不是让消费者猜测中间值。

并发 Saga 还可能相互覆盖。乐观并发控制可检测版本冲突,但发现冲突后要有业务决策:重试当前步骤、重新规划剩余步骤,还是补偿退出。技术上的 compare-and-set 不能代替业务规则。

编排和协同各有可见性代价

编排式 Saga 由协调器决定下一步并记录状态,流程图和失败位置较清晰,但协调器成为关键组件,需要高可用、版本兼容和避免承载过多领域逻辑。

协同式 Saga 由服务消费事件后触发下一事件,耦合表面较松,却可能让整体流程散落在订阅关系中。循环触发、重复事件和版本演进更难观察,必须有统一关联标识和流程视图。

两者可以组合:关键流程由工作流协调器管理,领域内部通过事件反应。选择依据是流程所有权、可审计性、变更频率和故障恢复需求,不是把事件驱动当作目标本身。

消息与数据库提交需要可靠连接

局部事务更新数据库后若发送消息失败,下游不会继续;先发消息再提交,消费者又可能看到尚未成立的状态。事务性发件箱把业务变更和待发事件写入同一数据库事务,再由发布器至少一次转发。

至少一次意味着消费者仍要幂等。消息去重记录、业务唯一约束和状态转换检查应共同工作。只在缓存里记消息 ID,缓存丢失后仍会重复执行。

事件模式和流程定义都要版本化。运行数天的旧实例可能在部署后继续收到回调,新代码必须能识别旧状态,或通过迁移把实例安全推进,不能假设发布时所有 Saga 都已结束。

运维重点是卡住和不确定的流程

应记录 Saga 标识、业务键、当前步骤、尝试次数、最后错误、下一重试时间和补偿进度。指标不仅统计成功率,还要看各状态停留时间、补偿率、人工介入量和未知结果数量。

重放工具要限制作用域并显示将执行的步骤。管理员“从头重跑”可能重复已提交动作,正确做法是从持久化状态继续,或显式创建一个新的修复流程。

测试需要注入每个步骤提交前后、消息发布前后和补偿过程中的故障,验证最终业务不变量。Saga 接受中间状态可见和补偿不完美,以换取长流程的可用与自治;设计质量取决于是否诚实表达这些代价。

资料依据