Saga 用局部事务和补偿组织长流程
说明长流程怎样拆为可提交步骤,并在后续失败时执行语义补偿、恢复重试和人工处置。
Saga 用局部事务和补偿组织长流程
跨多个服务的业务流程可能持续数分钟、数天,期间还要等待人工或外部机构。若用一个全局数据库事务锁住所有资源直到结束,参与者必须长期在线,阻塞和故障概率都会上升。Saga 把长事务拆为一系列可独立提交的局部事务,并为已完成步骤准备补偿。
补偿不是数据库回滚的远程版本。局部事务提交后,其结果可能已被其他流程看到;补偿只能通过新的业务动作修正影响,不能让历史观察消失。
每个步骤都要有明确提交边界
Saga 的一步应在单一资源边界内原子提交,例如创建订单、预留库存或登记付款请求。步骤成功后,流程状态必须持久化,再决定下一步。只在内存中推进会在进程崩溃后丢失已完成位置。
步骤接口要区分业务拒绝、可重试技术错误和结果未知。超时只说明调用方没有及时收到结果,不证明对方未执行。盲目重试可能重复扣款或重复发货,因此每次业务操作需要稳定幂等键和可查询结果。
局部提交顺序影响可补偿性。不可逆或代价高的动作应尽量后置;可预留再确认的资源,可采用“保留—确认—释放”而不是先永久消耗再补偿。
补偿表达业务上的反向动作
库存预留的补偿可以是释放预留,付款授权的补偿可以是撤销授权,已经结算的付款则可能只能退款。退款和原扣款是两条记录,期间的汇率、手续费和用户可见状态都可能不同。
补偿也可能失败。它需要自己的重试、幂等和告警策略,并且通常按已完成步骤的逆序执行。若补偿依赖的外部系统长期不可用,流程必须进入可识别的待人工处置状态,而不是无限静默重试。
有些动作无法真正补偿,例如邮件已读、实体设备已启动或信息已披露。设计时应明确不可逆点,在此之前增加确认,在此之后采用后续纠正和沟通,而不是声称系统能恢复到从未发生。
隔离弱化会暴露中间状态
Saga 各步骤之间允许其他事务交错,因此不具备单数据库事务那样的隔离。其他流程可能读到暂时预留的库存或处理中订单,并据此作出决定。
可以通过语义锁、状态机、版本检查和可撤销资源降低异常。例如订单处于 PAYMENT_PENDING 时禁止发货,库存记录区分可用与预留。状态含义应进入查询契约,而不是让消费者猜测中间值。
并发 Saga 还可能相互覆盖。乐观并发控制可检测版本冲突,但发现冲突后要有业务决策:重试当前步骤、重新规划剩余步骤,还是补偿退出。技术上的 compare-and-set 不能代替业务规则。
编排和协同各有可见性代价
编排式 Saga 由协调器决定下一步并记录状态,流程图和失败位置较清晰,但协调器成为关键组件,需要高可用、版本兼容和避免承载过多领域逻辑。
协同式 Saga 由服务消费事件后触发下一事件,耦合表面较松,却可能让整体流程散落在订阅关系中。循环触发、重复事件和版本演进更难观察,必须有统一关联标识和流程视图。
两者可以组合:关键流程由工作流协调器管理,领域内部通过事件反应。选择依据是流程所有权、可审计性、变更频率和故障恢复需求,不是把事件驱动当作目标本身。
消息与数据库提交需要可靠连接
局部事务更新数据库后若发送消息失败,下游不会继续;先发消息再提交,消费者又可能看到尚未成立的状态。事务性发件箱把业务变更和待发事件写入同一数据库事务,再由发布器至少一次转发。
至少一次意味着消费者仍要幂等。消息去重记录、业务唯一约束和状态转换检查应共同工作。只在缓存里记消息 ID,缓存丢失后仍会重复执行。
事件模式和流程定义都要版本化。运行数天的旧实例可能在部署后继续收到回调,新代码必须能识别旧状态,或通过迁移把实例安全推进,不能假设发布时所有 Saga 都已结束。
运维重点是卡住和不确定的流程
应记录 Saga 标识、业务键、当前步骤、尝试次数、最后错误、下一重试时间和补偿进度。指标不仅统计成功率,还要看各状态停留时间、补偿率、人工介入量和未知结果数量。
重放工具要限制作用域并显示将执行的步骤。管理员“从头重跑”可能重复已提交动作,正确做法是从持久化状态继续,或显式创建一个新的修复流程。
测试需要注入每个步骤提交前后、消息发布前后和补偿过程中的故障,验证最终业务不变量。Saga 接受中间状态可见和补偿不完美,以换取长流程的可用与自治;设计质量取决于是否诚实表达这些代价。
资料依据
- Hector Garcia-Molina、Kenneth Salem:Sagas,https://www.cs.cornell.edu/andru/cs711/2002fa/reading/sagas.pdf
- PostgreSQL Global Development Group:Transaction Isolation,https://www.postgresql.org/docs/current/transaction-iso.html