跳到内容

第 6 卷 / 第 15 章 / 第 1 课

15.1 一致性边界与 Saga:把长事务改成可恢复流程 ​

报名服务已经保留了名额,支付服务却没有及时回应。各自的数据库都正常提交了事务,用户仍不知道自己能不能参赛。

服务各自拥有数据后,跨服务业务流程无法继续依赖一个本地数据库事务。解决方向不是寻找一个“永不失败的分布式调用”,而是明确哪些不变量必须立即成立,哪些状态可以暂时不一致,以及失败后如何恢复。

先写不变量,再选模式 ​

以赛事报名为例:

  • 单场比赛的确认名额不能超过容量;
  • 同一玩家的同一笔报名付款不能被重复扣费;
  • 支付成功但报名失败时,必须退款或转人工;
  • 排行榜允许在一分钟内延迟更新。

前两项可能要求强约束,退款或人工处理需要可靠恢复,最后一项允许有界延迟。最终一致本身不承诺一分钟内完成;这个业务目标还需要延迟指标、告警和修复机制。若不先区分,团队会在所有数据上追求昂贵的强一致,或在关键资金状态上错误地接受“最终会好”。

尽量把必须原子成立的不变量放在同一个数据所有者内。服务边界若切穿核心不变量,再复杂的消息模式也只能补救糟糕的边界。

Saga 是一组本地事务 ​

Saga 把长流程拆成多个由不同服务提交的本地事务。业务步骤确定失败时,执行补偿动作处理已经提交的结果;瞬态失败可先重试,结果未知时应先查询或等待确认,不能把超时直接当作“从未执行”。

text
正常:预留名额 → 授权支付 → 确认报名
授权明确失败:释放名额
确认明确失败:撤销授权 → 释放名额

这里假设报名阶段只做支付授权;授权尚未扣款,实际扣款及其失败恢复是另一个必须定义的步骤。若已经扣款,补偿通常需要退款,不能沿用“撤销授权”。

补偿不是数据库回滚。退款可能产生手续费,通知无法“撤回记忆”,外部系统也可能已经继续处理。因此补偿必须具备业务语义,并允许人工介入。

编排与协同 ​

Orchestration ​

编排器显式记录流程状态并发送下一步命令:

text
Registration Saga
  ├─ ReserveSeat
  ├─ AuthorizePayment
  ├─ ConfirmRegistration
  └─ on failure: ReleaseSeat / VoidPayment

优势是状态、超时和补偿集中可见;风险是编排器吸收过多领域决策,成为跨服务“上帝对象”。

Choreography ​

服务订阅事实事件并独立反应:

text
SeatReserved → Payment authorizes
PaymentAuthorized → Registration confirms
RegistrationConfirmed → Notification sends

优势是发布者不必认识所有消费者;风险是完整流程散落在多个订阅者中,循环依赖、重复处理和失败位置更难观察。

简单、稳定、后续反应彼此独立时可用协同;步骤多、补偿复杂、需要明确 SLA 和人工操作时,编排通常更可控。

中间状态必须进入模型 ​

长流程不能只用“成功/失败”表示。报名可能处于:

text
PENDING_SEAT
PENDING_PAYMENT
CONFIRMED
CANCELLING
CANCELLED
MANUAL_REVIEW

Saga 不提供跨服务的事务隔离,其他流程会看到已经提交的中间结果。以上是状态名称示例,并非完整转换图:还要明确名额到期与支付迟到的竞态。比如已释放名额后才收到支付成功,就进入撤销或退款恢复,不能直接把报名改为已确认。

这些状态让重试、查询和人工处理具有依据。用语义锁或状态机阻止冲突操作,例如 PENDING_PAYMENT 时不能再次发起一笔新支付。

以下是项目中的条件更新片段;registration.id 必须唯一,:name 是数据库客户端绑定参数,不是直接粘贴到 psql 的变量语法。调用前必须已核验支付凭据、金额和有效名额;SQL 的状态与版本条件只负责防止并发覆盖:

sql
UPDATE registration
SET status = 'CONFIRMED', version = version + 1
WHERE id = :id
  AND status = 'PENDING_PAYMENT'
  AND version = :expected_version;

受影响行数为 0 仅表示没有匹配到这一状态和版本的记录,也可能是 ID 不存在或记录不可见。调用者应重新查询并决定幂等返回、恢复还是拒绝,不能一律归为可重试的版本冲突。

每一步都要定义恢复语义 ​

问题必须明确
命令重复是否幂等,幂等键保存多久
响应超时如何查询结果,而不是盲目重复执行
事件乱序依据版本拒绝、缓存还是重排
补偿失败是否重试,何时转人工
流程超时业务截止时间和自动取消策略
服务重启流程状态从哪里恢复

Saga 的可靠性来自持久化状态、可重复执行和可观测恢复,不来自一条顺利演示的 happy path。

下一课解决本地事务与消息发布之间的原子缺口,并建立消费者幂等与死信处置。

参考资料 ​

Built with VitePress | Software Systems Atlas