踩坑三年,我终于搞明白了分布式事务的那点事儿

模型调用员
2025-06-20 05:39
阅读 208

开头:那些年我踩过的分布式事务坑

开头:那些年我踩过的分布式事务坑

记得刚做后端架构师那会儿,公司正在从单体应用向微服务转型。系统模块拆得多了,业务复杂了,问题也开始变多了——尤其是跨服务的数据一致性成了一个老大难。

比如有一个订单系统和库存系统的交互场景:用户下单时需要同时扣库存、生成订单。这两个操作必须都成功,或者都失败。但它们分别属于不同的服务,部署在不同的节点上,数据也各自独立存储。这种情况下,传统数据库的本地事务机制已经无能为力了。

当时的我一脸懵:“这不就是个分布式事务吗?业界应该早有标准方案了吧?”结果一通调研才发现,根本没有银弹。每种方案都各有适用场景,也都有“雷”。

这篇文章,我想结合自己过去几年实际参与过的一些项目,来聊聊我在解决分布式事务问题过程中的真实踩坑经历、技术选择背后的考量,以及一些宝贵的运维经验。


项目背景:一次典型的业务场景

项目背景:一次典型的业务场景

我们的核心业务是一个电商系统,主要包括以下几个服务:

  • 订单服务(Order Service)
  • 库存服务(Inventory Service)
  • 支付服务(Payment Service)
  • 用户服务(User Service)

最典型的一个场景就是:用户点击【提交订单】按钮后,系统要做一系列操作:

  1. 创建订单记录
  2. 检查并扣除商品库存
  3. 如果开启货到付款,则等待物流确认;如果是在线支付,则调用第三方支付接口并更新支付状态

所有这些操作都需要保证整体的事务性 —— 要么全部成功,要么全部回滚。否则就会出现库存扣了但订单没创建、或订单创建了但没扣库存的问题,甚至引发资损。

刚开始我们尝试使用本地事务+补偿机制,后来随着并发量上来、业务流程变复杂之后,就出现了各种各样的异常情况,比如幂等处理不完整、数据不一致、事务长时间未完成等等。

当时我们就意识到:是时候认真对待分布式事务这个问题了


分布式事务挑战与踩过的坑

分布式事务挑战与踩过的坑

在探索的过程中,我踩了不少坑,挑几个印象深刻的说说:

坑一:盲目使用两阶段提交(2PC)

一开始我们想着用经典的两阶段提交协议(Two Phase Commit),认为这是最标准的分布式事务解决方案。

我们在多个关键操作中实现了一个简易版的2PC协调器,在订单服务里作为事务发起方,在库存服务中做参与者。但是上线不久就遇到了严重问题:

  • 性能差:每个事务都需要两次网络通信,整个下单链路耗时明显增加;
  • 可用性低:一旦某个服务挂掉,整个事务进入阻塞状态,用户体验极差;
  • 协调器单点故障:协调器崩溃后没有恢复机制,导致很多事务处于“不确定”状态。

最后不得不用一个定时任务来定期检查状态,手动干预数据一致性,非常头疼。

总结下来一句话:2PC不适合高并发、多节点、对可用性敏感的场景


坑二:误以为TCC就是万能解法

接下来我们听说了一个叫TCC(Try-Confirm-Cancel)的模式,看起来很适合我们的业务场景。于是大张旗鼓地重构了一波代码,把订单和库存操作都改成了TCC风格。

想法很好,但实践中遇到的问题比想象的要多得多:

  • 业务侵入性太强:每个方法都要写Try、Confirm、Cancel三个版本,开发成本陡增;
  • Cancel逻辑复杂:有些操作不具备可逆性,比如通知下游服务、消息发送出去了、短信已经发了,这时候想回滚根本不可能;
  • 数据一致性保障困难:网络延迟、Cancel失败等情况都需要额外补偿机制支持。

举个例子:当订单创建失败需要Cancel库存时,刚好库存服务不可用,这时库存就无法释放,变成“死库存”,后续又需要用额外脚本去清理。

这段经历让我深刻认识到:TCC不是银弹,它只适用于高度可控、可逆性强的操作


坑三:轻视异步化与幂等设计

我们曾经尝试使用MQ进行异步事务处理,比如将订单创建成功的消息推到Kafka,库存服务消费这个消息来执行库存扣除。

结果很快出问题了:

  • 消息重复消费导致库存被重复扣减;
  • 消息丢失导致订单创建成功但库存没扣,引起超卖;
  • 事务边界模糊:订单服务不知道什么时候该算真正完成。

这次教训让我们重新认识到了异步场景下幂等设计的重要性。

我们花了好几周才修复这个问题,最终是在库存服务加了全局ID + 状态机管理,并结合数据库乐观锁,才算稳定了下来。


我们的分布式事务实践路径

经过一段时间的折腾和反思,我们逐渐形成了一套比较成熟的分层策略,针对不同业务场景选用不同的事务控制手段:

场景 适用方案 使用时机
强一致性要求 Saga模式 下单流程、资金流转
高并发异步处理 最终一致性(MQ + 幂等) 日志落盘、消息推送、非核心流程
短周期、操作可逆 TCC 库存锁定与释放、优惠券发放
数据聚合分析类 不要求强一致性,直接容忍短时偏差 报表统计、数据打快照

下面重点讲一下我们目前最核心采用的两种方式:Saga模式 和 最终一致性模式。


实战一:用Saga模式搞定核心下单流程

Saga是一种事件驱动的长事务编排模型,核心思想是通过补偿动作来保障最终一致性

我们以订单创建为例,流程如下:

Order Created (Try)  
│  
├─→ Inventory Locked (Try)  
│  
└─→ Payment Preauth (Try)  

如果某一步失败,比如库存锁定失败,就开始逆序执行补偿:

Payment Preauth → Cancel  
Order → Cancel  

我们用的是基于事件驱动的状态机引擎(类似Netflix Conductor),定义了完整的状态流转规则。

实现细节分享:

  • 每个操作都是同步的,避免引入复杂的消息队列;
  • 每个步骤记录日志,用于重试、恢复和追踪;
  • 补偿逻辑单独封装,并在失败时自动触发;
  • 引入超时机制,防止事务长时间滞留;
  • 前端展示“处理中”状态,降低用户感知压力;

这套方案上线后,下单成功率提升了约40%,异常交易数量下降明显。虽然不能做到实时强一致,但事务的整体健壮性和可观测性大大增强


实战二:最终一致性 + MQ + 幂等保障异步流程

对于一些不需要强一致性的操作,比如给用户发邮件、记录行为日志、推送通知,我们采用了最终一致性方案:

  1. 订单服务下单完成后,往Kafka发一条消息;
  2. 库存服务消费这条消息,执行扣减;
  3. 消息设置唯一ID,消费方本地保存已处理ID集合;
  4. 扣减库存前先查是否已处理过此消息,避免重复;
  5. 同时配合数据库乐观锁控制并发扣减。

在这个过程中,我们还做了几个优化:

  • 使用Redis缓存消息ID,提高判断效率;
  • 设置消息过期时间,防止单条消息长期堆积;
  • 失败消息自动重试,最多三次,超过则人工介入;
  • 监控消息积压,及时告警通知负责人。

这个方案实施后,系统的吞吐能力大幅提升,同时避免了因为消息重复而导致的脏数据。


总结:分布式事务没有标准答案

从业务角度看,分布式事务本质是为了在复杂的系统间维持数据一致性。但现实是,没有任何一种方案能覆盖所有场景。

从我个人的经验来看:

  • 不要迷信某种“高级”方案,比如TCC、Seata之类,适配才是关键;
  • 不要过度追求强一致性,很多时候最终一致性+补偿机制更加实用;
  • 要重视可观测性,无论是日志记录、状态追踪还是监控体系;
  • 要为异常情况预留兜底手段,比如定时校验、人工审核、数据对账等;
  • 幂等设计贯穿始终,尤其是在异步场景中尤为重要;
  • 性能和可靠性之间要权衡取舍,有时候牺牲一点性能换取稳定性也是值得的;

系统架构设计图-1


给读者的一些建议

如果你也正面对分布式事务的问题,以下是我总结的一些小建议:

  1. 从业务角度出发:优先判断是否真的需要严格一致性。很多时候最终一致性就足够用了。
  2. 评估你的团队能力:TCC、Saga虽然强大,但如果团队缺乏经验,容易搞出更大的问题。
  3. 选型之前多画流程图:清晰描述你的业务流程和错误处理路径,再决定选用哪种模式。
  4. 从小处入手验证可行性:先在一个小模块尝试落地,别一上来就在核心链路上动刀。
  5. 做好降级准备:无论你用什么方案,生产环境总会出点幺蛾子。提前准备好回退和补偿措施。

尾声:技术只是工具,架构更需温度

分布式事务是个老生常谈的话题,但每次深入其中,都会有新的收获。

回头看这几年走过的弯路,其实都是成长的阶梯。每一个“翻车”的瞬间都在提醒我:技术的本质,是服务于业务。脱离业务的纯技术炫技,往往走不远。

希望这篇带着踩坑血泪史的文章,能帮你在面对分布式事务时少走些弯路。

如果你也有类似的经历,欢迎留言交流。一起在分布式的世界里,走得更稳、更远。


评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝