分布式事务:2PC太重、Synchronized太土,Saga才是微服务的体面退出方式
凌晨两点,你被电话叫醒。线上告警:用户下单成功,库存扣了,支付也扣了,但物流服务超时了。最骚的是,这三个服务分别属于三个不同的团队,数据库各管各的。
你的第一反应是:回滚啊。
然后你打开代码,发现库存服务的"回滚"是一个单独的HTTP接口,支付服务的回滚是另一个接口,物流服务的回滚接口还需要调用第三个服务做确认。而且没有任何分布式事务框架支撑这些操作,唯一的保障是你自己写的一段Synchronized。
恭喜,你踩进了分布式事务最经典的陷阱:在多个独立服务之间,试图用单进程思维解决跨进程问题。
分布式事务的"不可能性三角"
有个著名的CAP定理,说分布式系统只能在一致性、可用性、分区容错性中选两个。大多数人学过,但真正踩过坑才理解这句话的重量。
但今天我要说的是另一个三角:一个比CAP更隐蔽、更贴近日常开发的不可能性三角——
强一致性、高性能、跨服务——三者不可兼得,你必须放弃一个。
2PC(两阶段提交)选择了强一致性,但代价是性能极差,且协调者单点故障会导致整个系统hang住。生产环境中敢用2PC的团队,要么是银行核心系统,要么是对分布式一无所知。
Synchronized本地锁选择了高性能,但在多服务架构下完全失效——锁不住其他进程的数据库。
那么,有没有一种方式,能在保证基本一致性的同时,还保持良好的性能,并且天然适配微服务架构?
答案是Saga模式。
Saga是什么:拆掉大事务,变成一串小事务
Saga的核心思想非常简单粗暴:不要试图让多个服务共用一个事务,把一个大事务拆成多个顺序执行的本地事务,每个步骤都有对应的"反悔操作"(补偿事务)。
任何一个步骤失败了,就执行后续步骤的补偿操作,把之前成功的步骤逐一撤销。
听起来是不是有点像"事后诸葛亮"?没错,Saga就是一个"事后救火队"。它不阻止火灾发生,但它有完整的灭火预案。
举个例子。电商下单场景:
1. 库存服务:扣减库存(本地事务)
补偿:还原库存
2. 支付服务:扣款(本地事务)
补偿:退款
3. 物流服务:创建订单(本地事务)
补偿:取消订单
正常流程:1 → 2 → 3,全部成功。
异常流程(2失败):1成功,2失败。执行补偿1:还原库存。
异常流程(3失败):1、2成功,3失败。执行补偿2(退款),然后执行补偿1(还原库存)。
注意这里的顺序:补偿是逆序执行的,后成功的先补偿。
编排式 vs 编舞式:谁在指挥这场演出
Saga有两种实现方式,这是面试和实战中最容易混淆的点。
编舞式(Choreography):去中心化,各服务自扫门前雪
每个服务订阅事件,发布事件,靠事件链驱动整个流程。
库存服务:
- 订阅:OrderCreatedEvent → 扣库存 → 发布:InventoryReservedEvent 或 InventoryFailedEvent
支付服务:
- 订阅:InventoryReservedEvent → 扣款 → 发布:PaymentCompletedEvent 或 PaymentFailedEvent
物流服务:
- 订阅:PaymentCompletedEvent → 创建订单 → 发布:OrderCompletedEvent
补偿逻辑也靠事件驱动:PaymentFailedEvent触发库存还原,InventoryFailedEvent触发支付取消。
优点:服务间解耦,新加服务不需要改现有服务的代码。
缺点:流程不透明,调试困难,循环依赖风险高。当你说"我要查这个订单现在卡在哪个步骤"时,你会发现代码里根本没有答案——答案散布在四个服务的事件订阅逻辑里。
编排式(Orchestration):中心协调者,一切尽在掌控
一个专门的Saga Orchestrator(编排器)来指挥整个流程,每个步骤是同步调用。
SagaOrchestrator:
step1(): 调用库存服务扣库存
onStep1Success(): 调用支付服务扣款
onStep2Success(): 调用物流服务创建订单
// 补偿逻辑也在编排器里
compensate():
if (step3Done) cancelLogistics()
if (step2Done) refundPayment()
if (step1Done) restoreInventory()
优点:流程清晰,事务状态有地方统一管理,出问题好排查。
缺点:编排器是单点风险,但可以用状态机持久化解决。
实战中,建议用编排式。管理复杂度降低带来的收益,远大于去中心化带来的解耦收益。尤其是当你需要支持查询当前事务进度、或者做失败告警的时候,没有中心协调者你根本做不到。
一个真实踩坑案例:电商退货的Saga设计
说个我实际做过的场景。电商退货流程,涉及四个服务:
1. 售后系统:审核通过退货请求
2. 财务系统:生成退款单并退款
3. 库存系统:收到退货后入库
4. 会员系统:返还积分(积分换过商品的要扣回来)
如果用同步调用,一个服务挂了整个流程就卡住。而且每个服务的超时时间不一样,财务系统最慢,库存系统最快,混在一起就是灾难。
我们设计了这样一个Saga:
ReturnSagaOrchestrator:
状态机:PENDING → REFUNDING → REFUNDED → INVENTORY_RETURNING → COMPLETED
↓(失败)
COMPENSATING → COMPENSATED/FAILED
execute():
1. 调用售后系统审核 → 失败则直接结束
2. 调用财务系统退款 → 失败则进入补偿:撤销审核
3. 调用库存系统确认入库 → 失败则进入补偿:财务退款冲正 + 撤销审核
4. 调用会员系统返还积分 → 失败则进入补偿:库存取消入库 + 财务退款冲正 + 撤销审核
这里有个关键设计:补偿链路不是简单的逆序调用,而是根据实际进度计算最小补偿集。
第三步失败了?不需要补偿第一步(审核),因为审核的逆向操作(撤销审核)不会影响已经退完的钱。但第四步失败,就要把前面三步全部补偿回来。
另外,每个补偿操作本身也是幂等的。退款冲正要防止重复退款,库存还原要检查商品状态,防止恶意用户先损坏商品再申请退货。
Saga的五个地狱级陷阱
1. 补偿操作本身的失败
这是最容易被忽视的问题。Saga假设补偿一定会成功,但现实是网络超时、数据库故障、服务宕机,一个都不少。
对策:补偿也要有重试机制,但要设置最大重试次数,超过上限进入人工介入流程。同时做好补偿失败的告警。
2. 并发Saga之间的互相干扰
Saga不提供隔离性,两个并发的退货Saga可能同时修改同一批库存。
对策:业务层面加乐观锁,比如库存服务扣减时用版本号或状态机,扣库存接口要做防并发校验。
3. 循环依赖(编舞式的特产)
服务A → 服务B → 服务C → 服务A
choreography模式下,四个服务互相订阅事件,加一个新服务要改六个地方,而且循环依赖会导致某些事件被重复消费。
对策:优先选择编排式。必须用 choreography的话,用事件溯源(Event Sourcing)来保证事件处理的幂等性和顺序性。
4. 查询事务当前状态
当你需要对用户展示"您的订单现在在哪一步"时,编舞式会让你想哭。
对策:编排式 + 状态持久化。每个Saga实例的状态都存一张表(可以用MySQL或Redis),支持随时查询。
5. 补偿的顺序比业务逻辑更重要
一个常见错误:按业务顺序做补偿,而不是按逆序。
举例:退款成功,库存还原失败。应该先补偿退款还是先补偿库存?
答案:先补偿退款,再补偿库存。因为退款的资金风险更高(钱出去了就真的出去了),而库存的损失是可控的(商品还在,恢复了就能再卖)。
这个原则叫做风险优先原则:风险越大的操作,补偿优先级越高。
实战建议:什么时候用Saga,什么时候不用
Saga不是银弹。以下场景,Saga是好的选择:
- 订单创建、取消、退货等长事务场景
- 跨多个服务的用户注册/注销流程
- 需要异步处理、允许最终一致性的业务流程
- 微服务架构下需要解耦的服务协作
以下场景,Saga不是好的选择:
- 强一致性要求极高的金融核心交易 → 用2PC或XA事务
- 单服务内的本地事务 → 直接用数据库事务,加什么Saga
- 调用链非常浅(只有一层调用) → Synchronous + 本地事务可能更简单
最后说一句得罪人的话:很多团队用Saga,根本原因是微服务拆得太细了。本来两个服务合成一个就能用本地事务解决的事,非要引入分布式事务来增加复杂度。
架构的第一原则是:不要过度设计。能用一个数据库事务解决的事,不要引入消息队列。能用同步调用解决的事,不要引入Saga。先把问题简单化,再考虑复杂化。
Saga是工具,不是荣耀。用了Saga不说明你技术好,能不用就不需要用才是真正的本事。