分布式事务解决方案:我踩过的坑和学到的经验

延迟优化师
2025-06-19 18:03
阅读 569

在互联网行业工作了这么多年,分布式系统几乎是每个后端开发者都要面对的“成人礼”。尤其当你负责一个核心交易系统时,数据一致性就成了头等大事。而在这个过程中,分布式事务是绕不开的一道坎。

今天我就想结合自己亲身参与的一个真实项目案例,来聊聊我们在做一次跨多个服务、多个数据库的订单支付流程中,是如何一步步应对和解决分布式事务问题的,以及在这条路上踩过的一些坑和后来总结出的经验。

背景介绍:为什么我们需要处理分布式事务?

API接口文档-1

背景介绍:为什么我们需要处理分布式事务?

我之前负责的是电商平台的核心交易系统。这个系统的模块拆得比较细:有订单中心、支付中心、库存中心、用户中心、积分系统等多个微服务,各个服务之间通过 RPC 接口相互调用。

举个最典型也是最常见的业务场景:

用户提交订单后,需要:

  • 扣除库存(库存中心)
  • 更新订单状态为“已支付”(订单中心)
  • 生成支付记录并通知第三方支付平台(支付中心)
  • 增加用户的积分(用户中心)

这些操作分布在四个不同的服务里,使用各自的数据库。一旦其中任何一个步骤失败,整个流程必须回滚,否则就会出现钱扣了但库存没减,或者订单状态变好了但积分没加的情况——这就是典型的分布式事务需求。

问题描述:早期我们怎么做的?遇到了哪些问题?

问题描述:早期我们怎么做的?遇到了哪些问题?

起初我们为了快速上线,采用了一个简单的做法:本地事务 + 异步补偿机制(定时任务)

比如订单支付成功后:

  1. 订单中心将订单状态改为“已支付”;
  2. 支付中心记录一笔支付流水;
  3. 库存中心异步发送一个 MQ 消息去扣除库存;
  4. 用户中心也接收一个 MQ 消息增加积分。

理论上只要某个环节失败,定时任务会去扫描异常订单做补偿。

但现实很残酷:系统刚上线就出了好几个严重 bug:

  • 库存被多扣或漏扣:MQ 发送失败、消费者崩溃、重试逻辑不严谨
  • 积分发放异常:MQ消息重复消费导致积分数目翻倍
  • 订单状态与实际不符:网络超时引发状态不一致

虽然最终通过定时任务兜底能解决一部分问题,但用户体验很差,而且运维成本高到难以承受。更别说在高峰期,光看报警日志都能让你血压飙升。

所以我们意识到:这套“土办法”只能短期应急,真正要支撑高并发、高可靠的服务,必须引入更规范、更稳定的分布式事务方案。

解决思路:选型和权衡

负载均衡配置-2

解决思路:选型和权衡

当时市面上主流的方案有几个方向:

  1. 两阶段提交(2PC)
  2. TCC(Try-Confirm-Cancel)模式
  3. Saga 模式
  4. 基于消息队列的事务性消息
  5. Seata 等开源框架

我们团队做了对比分析,最终选择了以 Seata 为主、结合 TCC 的策略落地。理由如下:

  • Seata 提供了较完整的分布式事务支持,兼容 Spring Cloud 和 Dubbo
  • 我们业务场景偏向金融级,对一致性要求很高
  • 有些关键路径可以使用 AT 模式简化开发,性能也能接受
  • 对于非数据库资源(如积分操作),采用 TCC 模式进行补充

整体架构调整

我们将原有的订单支付流程改造为以下结构:

OrderService (TM) 
    → InventoryService (RM)
    → PayService (RM)
    → UserService (RM)

TM(Transaction Manager)即事务协调者,由 OrderService 扮演;其他服务作为 RM(Resource Manager),各自管理自己的数据库资源,并在全局事务中注册。

同时,我们在每台节点上都部署了 Seata Server(TC 组件),用来协调整个事务的状态流转。

关键代码实践:如何接入 Seata

关键代码实践:如何接入 Seata

下面是一个简化的代码示例,展示如何在一个订单支付方法中开启分布式事务:

@GlobalTransactional
public void payOrder(String orderId) {
    try {
        orderService.updateOrderToPaid(orderId); // 修改订单状态
        inventoryService.reduceInventory(orderId); // 扣库存
        payService.createPaymentRecord(orderId); // 创建支付记录
        userService.addUserPoints(orderId); // 增加积分
        
    } catch (Exception e) {
        throw new RuntimeException("支付失败", e);
    }
}

只需要加一个 @GlobalTransactional 注解,Seata 就会在背后完成:

  • 开启全局事务
  • 各个服务执行分支事务(Branch Transaction)
  • 最终提交或回滚整个事务

是不是特别简单?确实,Seata 的优势就在于它对业务代码侵入性较小,只需要注解就能控制事务边界。

数据库层面的配合

为了配合 Seata 的 AT 模式,我们的各个数据库表都加上了 undo_log 表,用于事务回滚时恢复原始数据:

CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB;

这是 Seata 实现自动补偿的重要前提。

遇到的坑与调试经验

说实话,接入 Seata 并不是一帆风顺的,下面几个坑都是我们真实踩过的,分享出来希望你们能避免。

坑1:事务传播性搞不清楚,结果没生效

一开始我们在调用其他服务的时候,是这样写的:

@GlobalTransactional
public void payOrder(String orderId) {
    orderService.updateOrderToPaid(orderId);
    otherService.invoke(); // 其他服务内部也有注解?
}

结果发现事务根本没起作用。查了半天才发现是因为默认的事务传播机制是 Propagation.REQUIRED,但跨服务调用时如果没有正确透传 xid,事务上下文就断开了。

解决方法:确保服务之间的调用走的是 Seata 自动注入的代理类,RPC 框架要做对应的集成,比如 Dubbo 或 Feign 需要引入适配器组件。

坑2:幂等性没有做好,导致重复扣款或库存

在某个压测环境中,我们发现部分订单的积分被加了两次,库存也被扣了两次。原因是某些分支事务回滚后重试,但下游服务没做幂等校验。

解决方法:所有分支操作必须带上唯一标识符(比如订单ID+服务类型),并在入库前检查是否已经执行过该事务分支。

坑3:Seata Server 宕机导致事务卡住

我们有一次生产环境升级 Seata Server,忘记先停业务流量,导致很多事务处于中间态(prepared),迟迟没法释放锁。

解决方法

  • 升级前务必做灰度发布和流量切换
  • 监控 Seata 日志中的 timeouthang 状态
  • 自动清理机制也要跟上(比如写个巡检脚本定期解锁)

效果与收益:稳定性和体验双提升

自从全面接入 Seata 并优化好相关流程后,整个系统的数据一致性得到了极大提升:

  • 订单支付成功率提高了 98%
  • 人工客服因数据不一致导致的咨询量下降 75%
  • 运维同学再也不用天天盯着定时任务跑补偿逻辑了
  • 同时,我们也实现了更好的可扩展性,在后续增加新的服务时,只要按 Seata 的规范接入,即可无缝加入事务流程

更重要的是:我们终于能自信地说,“我们的订单支付链路,现在是可靠的”。

总结与建议:写给后来者的几点心得

如果你也在设计分布式系统,或者正在考虑分布式事务的实现方案,我想给你几点实用建议:

✅ 方案选型要考虑长期维护成本

不要为了“快上线”牺牲可维护性。像 Seata 这样的框架虽然一开始需要花时间搭环境、做集成,但它带来的稳定性价值远高于短期节省的时间。

✅ 优先选择轻量级、侵入少的方案

除非你有非常复杂的业务规则,否则尽量少写 TCC 的 Confirm/Cancel 方法,太容易出错。AT 模式更适合大多数常规场景。

✅ 不要把所有业务压在一个大事务里

事务越长,出错概率越高,性能损耗也越大。合理拆分业务流程,必要时允许“最终一致”,反而更容易实现高性能。

✅ 注意幂等、重试、超时、限流等周边机制

这些都是分布式系统中不可忽视的配套工程。一个完善的事务系统不仅需要事务机制本身,还需要周边保障措施的支持。

✅ 加强监控和告警体系

建议将 Seata 的事务状态、各分支执行情况统一接入监控平台,做到问题第一时间发现,而不是靠客户投诉才知道“又有订单扣库存失败了”。


这是我个人从一次真实项目中学到的关于分布式事务的经验。说到底,真正的技术成长往往不是来自文档里的理论,而是来自那些凌晨三点还在查日志的日日夜夜。希望这篇文章对你有所启发,也欢迎留言交流你的经验和想法!

评论 0

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