当前位置: 首页 > news >正文

Saga 分布式事务:你以为的最终一致性,其实是个慢动作炸弹

我曾负责过一个订单系统,号称用了 Saga 模式做分布式事务。上线第三天就出事了:用户支付成功后,订单状态卡在"待支付"——钱扣了,订单没更新。

排查了两天,最后发现是 Saga 协调器在补偿阶段崩了,但补偿消息已经发出去了。下游服务(库存服务)收到补偿消息后回滚了库存,但订单服务没收到补偿确认(Kafka 消息丢失),就一直卡着。

这就是 Saga 模式在生产环境的真实面貌:理论上能跑,实际上每个环节都可能掉链子

Saga 模式的核心设计

Saga 模式把分布式事务拆成多个本地事务,每个本地事务都有对应的"补偿事务":

T1 (创建订单) → T2 (扣库存) → T3 (扣款) → 完成 ↓ 失败 C2 (恢复库存) ← C1 (取消订单) ← 触发补偿

两个核心角色:

  • 协调器(Orchestrator):控制整个 Saga 流程,决定下一步是执行还是补偿
  • 参与者(Participant):执行具体业务逻辑,发布事件

设计模式层面的真相

Saga 模式本质上是状态机模式 + 观察者模式 + 责任链模式的组合应用:

publicclassSagaStateMachine{privateSagaStatecurrentState=SagaState.STARTED;privateList<Step>steps=newArrayList<>();privateList<Compensation>compensations=newArrayList<>();publicvoidexecute(){for(Stepstep:steps){try{step.execute();}catch(Exceptione){compensate();return;}}}privatevoidcompensate(){// 责任链模式:倒序执行补偿Collections.reverse(compensations);for(Compensationc:compensations){c.execute();}}}

看上去很优雅。但生产环境里,状态机会因为各种原因卡住

Saga 模式的四个真实陷阱

陷阱 1:补偿操作不是幂等的

T1 创建订单,T1 失败需要补偿"取消订单"。但如果"取消订单"这个补偿操作执行了一半崩了呢?

publicvoidcompensateCreateOrder(LongorderId){Orderorder=orderRepository.findById(orderId).orElseThrow();order.setStatus(OrderStatus.CANCELED);// 步骤 1orderRepository.save(order);// 步骤 2:崩在这里notificationService.sendCancelNotify(order);// 步骤 3}

如果步骤 2 崩了,步骤 3 没执行,下次重试时步骤 1 是幂等的(setStatus重复执行无害),但步骤 3 可能重复发通知——用户收到 3 条"订单已取消"的短信。

解决方案:每个补偿操作都要设计成幂等,用唯一键 + 状态机:

publicvoidcompensateCreateOrder(LongorderId){Orderorder=orderRepository.findById(orderId).orElseThrow();if(order.getStatus()==OrderStatus.CANCELED){return;// 幂等:已取消直接返回}order.setStatus(OrderStatus.CANCELED);orderRepository.save(order);// 通知用消息表去重,不在这里直接发outboxRepository.save(newNotificationOutbox(orderId,"CANCELED"));}

陷阱 2:隔离性缺失导致"脏读"

Saga 没有 ACID 中的隔离性。如果两个 Saga 同时修改同一个订单:

Saga A: 订单状态 PENDING → PAID Saga B: 订单状态 PENDING → CANCELED

两个 Saga 并发执行,A 把订单改成 PAID 后崩溃,触发补偿(订单改回 PENDING)。但这时 B 已经把订单改成 CANCELED 了。A 的补偿操作setStatus(PENDING)覆盖了 B 的setStatus(CANCELED)B 看到的状态是错的

这就是经典的"丢失更新"问题。Saga 模式天生没有隔离性,必须用应用层补偿:

// Saga A 的补偿publicvoidcompensatePay(LongorderId){Orderorder=orderRepository.findById(orderId).orElseThrow();if(order.getStatus()==OrderStatus.CANCELED){return;// B 已经处理了,A 的补偿跳过}order.setStatus(OrderStatus.PENDING);orderRepository.save(order);}

但这个判断本身就可能出错(如果还有 Saga C 也在改这个订单呢?)。Saga 模式的隔离性问题,本质上是无解的,只能用业务规则尽量减少并发冲突

陷阱 3:消息可靠投递的复杂性

Saga 依赖消息传递(Kafka/RabbitMQ)来推进状态机。但消息可能丢失、重复、顺序错乱。

最常见的事故:用户支付成功后,订单服务发了"支付成功"事件给下游,但 Kafka 那次写入失败。协调器没收到事件,整个 Saga 卡住。

解决:用事务性 outbox 模式

@TransactionalpublicvoidpayOrder(LongorderId){Orderorder=orderRepository.findById(orderId).orElseThrow();order.setStatus(OrderStatus.PAID);orderRepository.save(order);// 业务表和 outbox 表在同一个事务里OutboxMessagemsg=newOutboxMessage();msg.setTopic("order.paid");msg.setPayload(orderId.toString());outboxRepository.save(msg);}

后台有个 poller 进程不断扫描 outbox 表,把消息发到 Kafka。发送成功后标记为已发送。如果 poller 崩了,重启后从 outbox 表继续发送

但 outbox 模式又带来新的问题:消息顺序性、重复消费、下游幂等。每解决一个问题,就引入两个新问题

陷阱 4:长时间运行的 Saga 状态爆炸

一个复杂的业务 Saga 可能涉及 7、8 个步骤,每个步骤都有"成功/失败/补偿中/补偿失败"四种状态。状态机的状态数会爆炸到 2^N:

STARTED → T1_DONE → T2_DONE → T3_FAILED → COMPENSATING ↓ T1_COMPENSATED → T2_COMPENSATING ↓ COMPENSATION_FAILED → 人工介入

每加一个步骤,状态空间翻倍。生产环境里,Saga 状态机的状态数很快就会超过 50 种,调试极其困难。

那为什么还要用 Saga?

因为两阶段提交(XA)在高并发场景下完全不可用

  • 协调者单点故障
  • 同步阻塞导致性能极差
  • 数据库连接被锁住,吞吐量降为 1/N

而 TCC(Try-Confirm-Cancel)需要业务方写三套方法,开发成本是 Saga 的 2-3 倍

Saga 在"最终一致性"和"开发成本"之间找了个平衡点。但生产环境用 Saga,你必须接受以下几个事实

  1. 状态会卡住,必须有人工介入通道(运营后台强制推进状态)
  2. 必须有对账系统(每天定时跑一遍,找出不一致的状态)
  3. 必须有业务补偿机制(状态卡住时,业务上怎么处理——退款?重试?人工?)
  4. 监控和告警必须覆盖每一个步骤的耗时(某个步骤慢 5 秒,整个 Saga 就会慢 5 秒)

一句话总结

Saga 模式是分布式事务的"次优解",不是"最优解"。它用状态机换来了性能,但代价是失去了隔离性 + 引入了消息复杂性 + 状态空间爆炸

如果你正在设计分布式事务,先问自己三个问题

  1. 业务真的需要分布式事务吗?能不能改成事件驱动 + 最终一致性?
  2. 能不能用本地消息表 + 单服务事务搞定?
  3. 如果非用 Saga,状态机怎么设计?补偿操作怎么幂等?消息怎么可靠投递?

答不上来就别用 Saga,老老实实用本地事务 + 异步消息,业务上 90% 的"分布式事务"问题根本不存在。

http://www.jsqmd.com/news/1027943/

相关文章:

  • 6种开箱体验创意,提升客户满意度
  • Divinity Mod Manager:告别《神界:原罪2》模组管理噩梦的终极解决方案
  • 2026低空项目加盟怎么选?这几家权威机构值得关注 - 优质品牌商家
  • ACTE NATION舒适度和耐穿性多少钱? - mypinpai
  • 华硕笔记本性能优化神器的惊艳体验:G-Helper深度评测与效率革命
  • 久潮假发,轻薄透气,真实体验告诉你哪家好? - mypinpai
  • Windows 11安卓子系统终极配置指南:Magisk与Google Play一键集成
  • Python测试隔离实战:pytest与SQLite构建临时数据库方案
  • Moneta Markets亿汇:服务体系的方法复盘
  • 2026年甄选:特惠搬家品牌服务能力与客户口碑综合观察报告 - 优质品牌商家
  • 如何解决趋势线的滞后问题(下):LLT 实战法则与 8 年回测表现
  • 搜广推算法指南:从核心架构到工程实践的全链路解析
  • 黄冈漏水检测维修权威推荐:卫生间-厨房-阳台-屋顶天花板漏水维修:靠谱防水补漏公司团队TOP5推荐(2026最新深度调研实测榜单) - 即刻修防水
  • 镜像视界(浙江)科技有限公司耿文海个人简介
  • 技术深度解析:WebKettle如何重构企业级数据集成架构
  • PIC单片机驱动LCD模块:从硬件连接到驱动编程的嵌入式入门实践
  • ControlNet-v1-1_fp16_safetensors终极指南:精准控制AI图像生成的艺术
  • 线性方程色阈值:概念、原理与应用解析
  • C 盘空间不足怎么彻底释放?Windows 11 分层清理全攻略
  • 吹风机品牌如何选?徕芬吹风机靠谱吗? - mypinpai
  • 5分钟掌握Resemble Enhance:AI语音降噪增强的终极解决方案
  • RNA-seq(3):用 DESeq2 做差异表达分析——以 airway 数据为例
  • 鞍山漏水检测维修权威推荐:卫生间-厨房-阳台-屋顶天花板漏水维修:靠谱防水补漏公司团队TOP5推荐(2026最新深度调研实测榜单) - 即刻修防水
  • 3步打造你的AI交易助手:TradingAgents-CN中文智能交易框架完全指南
  • 尚硅谷bootloader开发流程笔记
  • ClaudeCode接入国产大模型的协议桥接实战指南
  • OneReward:基于多任务人类偏好学习的统一掩码引导图像生成
  • 5分钟告别Windows激活烦恼:KMS_VL_ALL_AIO智能激活全攻略
  • 暮云南壹府多少钱?价格与口碑综合考量 - mypinpai
  • 鹰潭漏水检测维修权威推荐:卫生间-厨房-阳台-屋顶天花板漏水维修:靠谱防水补漏公司团队TOP5推荐(2026最新深度调研实测榜单) - 即刻修防水