15.1 一致性边界与 Saga:把长事务改成可恢复流程
报名服务已经保留了名额,支付服务却没有及时回应。各自的数据库都正常提交了事务,用户仍不知道自己能不能参赛。
服务各自拥有数据后,跨服务业务流程无法继续依赖一个本地数据库事务。解决方向不是寻找一个“永不失败的分布式调用”,而是明确哪些不变量必须立即成立,哪些状态可以暂时不一致,以及失败后如何恢复。
先写不变量,再选模式
以赛事报名为例:
- 单场比赛的确认名额不能超过容量;
- 同一玩家的同一笔报名付款不能被重复扣费;
- 支付成功但报名失败时,必须退款或转人工;
- 排行榜允许在一分钟内延迟更新。
前两项可能要求强约束,退款或人工处理需要可靠恢复,最后一项允许有界延迟。最终一致本身不承诺一分钟内完成;这个业务目标还需要延迟指标、告警和修复机制。若不先区分,团队会在所有数据上追求昂贵的强一致,或在关键资金状态上错误地接受“最终会好”。
尽量把必须原子成立的不变量放在同一个数据所有者内。服务边界若切穿核心不变量,再复杂的消息模式也只能补救糟糕的边界。
Saga 是一组本地事务
Saga 把长流程拆成多个由不同服务提交的本地事务。业务步骤确定失败时,执行补偿动作处理已经提交的结果;瞬态失败可先重试,结果未知时应先查询或等待确认,不能把超时直接当作“从未执行”。
正常:预留名额 → 授权支付 → 确认报名
授权明确失败:释放名额
确认明确失败:撤销授权 → 释放名额这里假设报名阶段只做支付授权;授权尚未扣款,实际扣款及其失败恢复是另一个必须定义的步骤。若已经扣款,补偿通常需要退款,不能沿用“撤销授权”。
补偿不是数据库回滚。退款可能产生手续费,通知无法“撤回记忆”,外部系统也可能已经继续处理。因此补偿必须具备业务语义,并允许人工介入。
编排与协同
Orchestration
编排器显式记录流程状态并发送下一步命令:
Registration Saga
├─ ReserveSeat
├─ AuthorizePayment
├─ ConfirmRegistration
└─ on failure: ReleaseSeat / VoidPayment优势是状态、超时和补偿集中可见;风险是编排器吸收过多领域决策,成为跨服务“上帝对象”。
Choreography
服务订阅事实事件并独立反应:
SeatReserved → Payment authorizes
PaymentAuthorized → Registration confirms
RegistrationConfirmed → Notification sends优势是发布者不必认识所有消费者;风险是完整流程散落在多个订阅者中,循环依赖、重复处理和失败位置更难观察。
简单、稳定、后续反应彼此独立时可用协同;步骤多、补偿复杂、需要明确 SLA 和人工操作时,编排通常更可控。
中间状态必须进入模型
长流程不能只用“成功/失败”表示。报名可能处于:
PENDING_SEAT
PENDING_PAYMENT
CONFIRMED
CANCELLING
CANCELLED
MANUAL_REVIEWSaga 不提供跨服务的事务隔离,其他流程会看到已经提交的中间结果。以上是状态名称示例,并非完整转换图:还要明确名额到期与支付迟到的竞态。比如已释放名额后才收到支付成功,就进入撤销或退款恢复,不能直接把报名改为已确认。
这些状态让重试、查询和人工处理具有依据。用语义锁或状态机阻止冲突操作,例如 PENDING_PAYMENT 时不能再次发起一笔新支付。
以下是项目中的条件更新片段;registration.id 必须唯一,:name 是数据库客户端绑定参数,不是直接粘贴到 psql 的变量语法。调用前必须已核验支付凭据、金额和有效名额;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。
下一课解决本地事务与消息发布之间的原子缺口,并建立消费者幂等与死信处置。
参考资料
- Chris Richardson, Saga
- AWS Prescriptive Guidance, Saga orchestration pattern