分布式事务的“坑”与“药”:一个架构师的真实实践分享

温柔导师
2025-06-25 14:49
阅读 388

分布式系统发展到今天,已经几乎成为大型后端服务的标配。而随之而来的“分布式事务”问题,也成为我们绕不开的技术难题之一。我亲身经历过几个典型的项目,在处理库存、订单、支付等多个子系统交互时,遇到了各种各样的数据一致性问题。这篇文章就想结合我的实际工作经验,聊聊我在解决分布式事务过程中踩过的“坑”,以及后来找到的一些“药”。

背景介绍:一次“失败”的促销活动

背景介绍:一次“失败”的促销活动

事情要从大约两年前说起。我当时在一家电商平台做后端架构师,平台当时刚经历一轮业务扩展,把原本单体应用拆分成了多个微服务模块:商品中心、库存服务、订单服务、支付服务、用户中心等。虽然结构清晰了,但随之而来的问题也开始频繁暴露。

有一次大促活动中,我们在凌晨上线了一个优惠券核销流程的更新版本。活动一开始,用户疯狂抢购,但短短十几分钟后,客服部门就炸锅了——有大量用户反馈明明扣了钱,却没有生成订单;更严重的还出现了库存扣减和订单创建不一致的情况,比如库存已经被扣光了,但订单却没下成功。

经过紧急排查,我们发现是多个服务之间的数据操作缺乏强一致性保障,最终导致状态紊乱。这个问题的本质就是分布式事务未正确处理

问题分析:为什么会出这样的问题?

问题分析:为什么会出这样的问题?

当时我们的做法其实很常见:为了保证性能,每个服务都是异步通信为主,用的是MQ来完成事件驱动。例如:

  • 用户下单 → 创建订单 → 发消息给库存服务扣减库存
  • 支付成功 → 发消息通知订单服务更新状态

但在高并发场景下,这种模式很容易出现问题。比如在订单写入数据库失败但库存已经被扣除的情况下,或者支付完成后消息丢失导致订单状态未能更新……

这其实就是典型的CAP问题下的取舍:我们在可用性和一致性之间偏向了前者,结果换来的是数据错误甚至账务损失。

总结下来,我们遇到的核心挑战可以归为以下几点:

  1. 跨服务的资源锁定困难:传统数据库的事务机制无法横跨多个服务。
  2. 网络不确定性带来的幂等性、可靠性问题:调用链路上任何一个环节失败都可能造成状态不一致。
  3. 补偿机制缺失或不可靠:没有一套完整且自动的回退策略,需要依赖人工介入修复。
  4. 日志和监控不到位:出错时排查效率低,定位周期长。

解决思路:不是所有问题都需要两阶段提交

解决思路:不是所有问题都需要两阶段提交

在面对这些问题时,我也开始查阅资料、调研业界常用的分布式事务方案。很多文章动辄提到TCC(Try-Confirm-Cancel)、Saga模式、Seata框架、两阶段提交(2PC)等等,但这些技术都有其适用场景和代价。

结合我们团队的技术栈(Java + Spring Boot + MySQL + RocketMQ)、性能要求、运维能力等因素,最终我们选择了一套轻量级、可落地、适合中大规模电商系统的解决方案组合:

1. 最终一致性 + 补偿机制(本地事务表 + 消息队列)

这是我们采用的第一层方案,也是目前使用最广泛的轻量级方法之一。

实现方式:

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

  • 订单服务在本地数据库插入订单记录的同时,写入一条“事务日志”(也叫本地事务表),标记该订单是否关联库存已扣减;
  • 成功入库后向 MQ 投递扣减库存的消息;
  • 库存服务消费消息,执行扣减操作,并将结果反向上报至订单服务;
  • 如果上报失败,则触发定时任务重试+补偿逻辑。

优点很明显:对原有业务侵入小,性能好,实现成本低;缺点是可能出现短时间内的不一致,需要容忍一定延迟。

我们用了 RabbitMQ 和 RocketMQ 都做过尝试,最后选择了 RocketMQ,主要是因为它有更好的顺序性保证和更高的吞吐能力。

小插曲:

刚开始上线的时候,有个同事忘了给 MQ 的消费者加幂等处理,结果因为网络波动消息被重复投递,导致同一笔订单扣了两次库存。这个教训让我们意识到:补偿必须是幂等的、可追溯的。

所以后来我们统一加了全局唯一ID + Redis缓存去重的机制,避免重复执行。


2. TCC 方案用于关键核心路径:如支付

对于像支付这样对一致性要求极高的操作,我们就采用了TCC 模式。TCC 的理念是将事务分为三个阶段:

  • Try:资源预留(冻结金额、预占库存)
  • Confirm:正式提交(完成交易、释放资源)
  • Cancel:回滚操作(取消冻结)

我们在支付流程里是这样设计的:

  • 用户点击支付 → 向资金服务发送 Try 请求,冻结账户余额;
  • 冻结成功 → 向订单服务发确认请求;
  • 若订单确认失败 → 触发 Cancel,解冻余额。

整个过程在服务内部通过接口协调完成,TCC 框架我们用的是开源的 Hmily,不过我们对其做了不少定制化改造以适配我们自己的业务模型。

这套方案在双十一大促中表现稳定,几乎没有出现重大故障。但我们也有一个痛苦的经验点:TCC 对业务逻辑的入侵非常大。你需要为每个操作手动设计 Try、Confirm、Cancel 三个步骤,开发成本高、维护复杂。


3. 日常运维 + 监控告警兜底:不能忽视的人工防线

再好的自动化系统也不是100%可靠的,尤其在复杂的生产环境中。所以我们建立了一个“事后检查+异常预警+人工修复”的最后一道防线:

  • 埋点采集关键状态节点,如“支付完成未生成订单”、“库存扣减无订单”;
  • 使用 ELK 收集日志并接入 Prometheus + Grafana 做实时监控;
  • 编写定时任务进行数据对账,发现不一致时自动触发补偿或标记待人工处理;
  • 开发了一个简易的后台页面供运营人员手动干预数据异常情况。

这部分其实是我们后期才补上的,初期总觉得“只要程序没问题就不用人管”,结果真出了问题才发现,缺少一个可视化的修复入口是多么痛苦的一件事。所以强烈建议大家一开始就设计好这些机制。


效果总结:稳定性提升明显,但并非银弹

从实施上述方案之后,我们平台的分布式事务相关故障率降低了90%以上,特别是在大促期间,基本没有出现数据错乱、重复扣款等问题。而且由于引入了完善的日志和监控体系,出问题后的排查效率也大大提升。

数据流转过程-2

但这套方案也不是万能的:

  • 它需要团队成员具备一定的设计能力和规范意识;
  • 需要长期投入去完善补偿机制和监控;
  • 在某些极端情况下,仍然会有短暂的数据不一致。

因此,分布式事务从来不是某一种技术就能完全搞定的,它是一整套工程实践、团队协作、监控兜底的结果。


经验分享:写给还在挣扎的你

服务器部署方案-1

如果你正在被分布式事务的问题困扰,不妨参考一下我这几年走过来的路:

✅ 推荐的做法:

  1. 根据业务需求选择不同强度的一致性策略

    • 对于非关键路径的操作(比如积分变化、通知类),用最终一致性即可;
    • 对于资金、库存、核心订单流,考虑使用 TCC 或 Saga 等补偿型事务;
    • 不要一股脑全用强一致,否则系统会慢得你怀疑人生。
  2. 做好日志与埋点
    所有事务流转的关键节点都要记录上下文信息,包括 traceId、操作类型、当前状态等,方便后续排查。

  3. 一切操作必须幂等
    这个原则适用于所有网络请求、MQ消费、回调、补偿。一定要在代码层面上强制约束。

  4. 设计补偿机制时保留历史记录
    不要只想着“怎么恢复”,还要知道“为什么错了”。历史记录不仅有助于排障,也为审计提供了依据。

  5. 运维视角也要提前规划
    异常检测、自动修复、人工干预工具尽早搭建起来。不要等到出事了再来补救。


❌ 不推荐的做法:

  1. 不要轻易使用两阶段提交(2PC)
    性能差、耦合高、容错差,适合金融级别的强一致性系统,但不适合互联网公司。

  2. 不要过度依赖事务框架本身
    框架只是辅助手段,真正的难点还是业务建模和规则梳理。别指望导入一个中间件就能解决问题。

  3. 不要忽视数据库设计的重要性
    有些看似是“事务问题”,其实是数据库设计不合理造成的。比如字段冗余、缺乏主外键、索引不合理等,都会加剧状态同步的难度。


结语:分布式事务是门艺术,更是种修行

一路走来,我越来越觉得,分布式事务不是一个技术问题,而是一个工程问题,甚至是一个组织文化问题。它考验着你的架构设计能力、团队协作水平、产品理解深度、甚至是运维文化的成熟度。

我们永远无法做到“绝对一致”,但我们可以无限接近。就像我们常说的一句话:“允许暂时的不同步,但绝不能容忍最终的不一致。”

希望这篇文章能给你一些启发,少踩些坑。也欢迎你在评论区留言交流,我们一起成长!


如果你喜欢这类实战派的架构文章,欢迎关注我后续的技术分享,我会继续带来更多来自一线经验的思考与实践。

评论 0

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