分布式事务的“坑”和我踩过的那些路:一个老后端工程师的实战分享

黄刚△
2025-06-27 23:43
阅读 550

大家好,我是某互联网公司的一名后端工程师,算上实习差不多干了五年。这五年里经历了从单体应用到微服务架构的转变,也见证了公司业务的快速扩张和系统复杂度的指数级增长。

说到微服务,很多人第一反应是模块化、高可用、易于扩展,没错,这些都是它带来的好处。但有一样东西,几乎每个做过微服务项目的同学都深有感触——分布式事务问题,简直是个“雷区”,稍不注意就炸得满地找人头。

今天我想结合自己亲身经历的几个项目,特别是去年年底接手的一个核心订单交易系统重构项目,来聊聊我们在面对分布式事务时遇到的真实挑战、尝试过的各种方案,以及最终落地的最佳实践。希望这篇文章能帮你在开发或运维过程中少走点弯路。


一、问题描述:为什么我们会掉进“事务”的坑?

一、问题描述:为什么我们会掉进“事务”的坑?

事情要从一个订单履约系统的重构说起。

这个系统负责处理电商平台的核心交易流程,包括下单、库存扣减、支付状态变更、物流分发等多个环节。在单体架构下,所有操作都在一个数据库中完成,通过本地事务就能保证一致性。但是随着业务发展,我们将整个系统拆成了多个微服务:

  • 订单服务
  • 库存服务
  • 支付服务
  • 物流服务

虽然功能划分清晰了,但新的问题也随之而来:

用户支付成功后,库存没扣,导致超卖;
或者库存扣了,但订单更新失败,用户投诉找不到订单;
更可怕的是,有时候某个服务宕机,事务卡在中间,数据出现不一致。

我们一开始想着用传统事务机制来解决这个问题——比如让订单服务调用库存服务的时候开启事务,但很快发现完全行不通:

  1. 跨服务事务无法使用本地数据库事务
  2. 接口调用可能失败或超时
  3. 消息队列的可靠性、幂等性、重试逻辑都需要处理

于是我们面临两个选择:

  • 继续用笨办法写大量补偿逻辑,每天盯着日志看哪里数据不一致;
  • 找到一种靠谱的分布式事务方案,在性能和一致性之间找到平衡。

很明显,前者效率低、出错多、运维成本高。我们选择了后者。


二、解决方案:选型与落地思路

二、解决方案:选型与落地思路

既然决定引入一套可靠的分布式事务方案,那我们就得选型。当时我们评估了常见的几种方式:

1. 两阶段提交(2PC)

优点是强一致性,缺点也很明显:性能差、协调者故障会导致系统阻塞、实现复杂度高。

2. TCC(Try-Confirm-Cancel)

比较适合业务场景明确的系统,比如库存扣减 + 支付确认这种,可以拆成 Try 阶段预占资源,然后 Confirm 提交或者 Cancel 回滚。但开发工作量大,每一步都要考虑回滚逻辑,而且幂等性和异常情况处理非常繁琐。

3. 消息事务 + 最终一致性

这种方式更轻量,利用消息队列解耦事务操作,通过异步消费实现最终一致性。适合容忍短时间延迟的场景,比如通知类或非实时关键路径的操作。

4. Seata 分布式事务框架

阿里开源的 Seata 是目前社区支持较好的一个分布式事务框架,支持 AT(自动补偿)模式、TCC 模式、Saga 模式等。AT 模式对代码侵入较小,适合我们这种不想大改业务逻辑的团队。

最终,我们选择了 Seata 的 AT 模式 + RocketMQ 异步补偿 的组合方案。

为啥这样搭配?

  • Seata 能够支持大多数业务操作,并且只需要加一行注解就可以实现分布式事务控制;
  • RocketMQ 做异步补偿是为了应对极端情况下 Seata 失败或网络不稳定的情况;
  • 双管齐下,既能在正常场景下保持高性能,也能在异常场景下保证最终一致性。

三、代码实践:核心代码片段解析

三、代码实践:核心代码片段解析

接下来我会给出一些项目中的核心配置和代码示例,帮助你理解如何将 Seata 和 RocketMQ 整合到业务中。

1. Seata 配置(application.yml)

seata:
  enabled: true
  application-id: order-service
  tx-service-group: my_tx_group
  service:
    vgroup-mapping:
      my_tx_group: default
    grouplist:
      default: 127.0.0.1:8091
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
      data-id: seataServer.properties
  registry:
    type: nacos
    nacos:
      application: seata-server
      server-addr: 127.0.0.1:8848

这里我们用了 Nacos 作为配置中心和注册中心,Seata 会从配置中心拉取自己的配置信息,同时注册自身地址给其他服务。

2. 事务主入口(OrderService)

@GlobalTransactional
public void placeOrder(OrderDTO order) {
    // 1. 创建订单
    orderDao.create(order);

    // 2. 调用库存服务
    inventoryService.deductInventory(order.getProductId(), order.getProductCount());

    // 3. 调用支付服务
    paymentService.charge(order.getUserId(), order.getTotalAmount());

    // 4. 更新订单状态为已支付
    order.setStatus("paid");
    orderDao.updateStatus(order);
}

是不是很简单?只需要加上 @GlobalTransactional 注解,就能让 Seata 帮我们管理整个事务链路。Seata 会在各个服务间开启全局事务,并自动维护事务上下文。

如果某一步骤失败,Seata 会自动触发回滚操作。

3. RocketMQ 补偿机制(定时+重试)

尽管 Seata 的稳定性很高,但我们依然担心极端情况下的失败,例如网络抖动导致事务未正确提交。为此,我们加了一个基于 RocketMQ 的兜底补偿机制:

@Scheduled(fixedRate = 5000)
public void checkPendingOrders() {
    List<Order> pendingOrders = orderDao.listByStatus("processing");

    for (Order order : pendingOrders) {
        try {
            // 再次尝试提交事务(这里可以加一次 Seata 的重试)
            retryPlaceOrder(order);
        } catch (Exception e) {
            // 发送消息到 MQ
            rocketMQTemplate.convertAndSend("ORDER_RETRY_TOPIC", order);
        }
    }
}

消费者监听该 Topic 并进行后续处理,避免因为一次失败就彻底丢失事务逻辑。


四、踩坑经验:哪些坑我走过,你千万别踩

四、踩坑经验:哪些坑我走过,你千万别踩

作为一个亲自部署过 Seata 的开发者,我可以很负责任地说:别光看文档,实际生产环境远比你想得复杂得多。

1. 数据库 undo_log 表没建,直接崩溃!

Seata 的 AT 模式依赖数据库的 undo_log 表来做事务回滚。如果你忘记在每一个参与事务的数据表中创建这张表,服务启动时不会报错,只有在发生回滚时才会抛出异常,极其隐蔽。

解决方法:

确保每个参与服务的数据源都执行以下 SQL:

CREATE TABLE `undo_log` (
  `id` BIGINT(20) NOT NULL AUTO_INCREMENT,
  `branch_id` BIGINT(20) NOT NULL,
  `xid` VARCHAR(100) NOT NULL,
  `context` VARCHAR(255) NOT NULL,
  `rollback_info` LONGBLOB NOT NULL,
  `log_status` INT(11) NOT NULL,
  `log_created` DATETIME NOT NULL,
  `log_modified` DATETIME NOT NULL,
  `ext` VARCHAR(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

2. Seata 的锁机制引发性能瓶颈

我们初期在一个高并发秒杀活动中发现,Seata 会在写操作时对记录加锁,防止脏读,但在并发高的场景下会出现严重排队现象,TPS 直接下跌。

解决方法:

调整 Seata 的隔离级别,关闭悲观锁机制(设置为 READ UNCOMMITTED),并通过应用层做冲突检测。

3. 微服务重启后事务恢复失败

Seata 会在宕机后试图恢复事务,但由于服务重启后本地事务 ID 可能变化,导致某些事务未能及时回滚,出现了数据残留。

解决方法:

引入 RocketMQ 定时补偿任务,并在 Redis 中记录事务状态,保证即使 Seata 失效,也能人工干预或自动修复。


五、效果总结:我们收获了什么?

自从这套方案上线以来,订单履约系统的数据一致性得到了极大提升。下面是几个显著的变化:

指标 上线前 上线后
数据不一致率 0.2%~0.5% < 0.01%
事务平均耗时 300ms 180ms
运维人力投入 每周需人工修复数据 几乎无需介入
服务稳定性 偶尔因事务失败导致雪崩 架构健壮性大大增强

此外,我们还观察到开发效率的提高——开发人员不再需要手动编写大量补偿逻辑,减少了 bug 的产生几率。


六、经验分享:给你几点建议

作为一名经历过几次分布式系统重构的老兵,我有几个建议想送给正在或者准备引入分布式事务的朋友:

1. 不要盲目追求“强一致性”

很多开发同学总觉得分布式事务必须做到像本地事务一样“百分百一致”。其实不然。在大多数业务场景中,只要保证“最终一致性”即可,比如订单、支付、物流这些,允许几秒钟甚至几十秒的延迟。

不要为了追求“绝对正确”,牺牲了性能和可维护性。

2. 技术方案不是万能钥匙

Seata 再牛,也只是个工具。它的适用性受限于你的数据库、业务模型和系统复杂度。比如 MySQL 的版本是否兼容、是否启用了 binlog、是否有读写分离等问题都会影响其表现。

所以在选型之前,一定要做好压测和全链路测试,不能只靠文档说的那样去判断。

3. 引入监控很重要

我们搭建了一套完整的 Seata + Prometheus + Grafana 的监控体系,能够实时查看:

  • 正在运行的全局事务数
  • 事务提交失败率
  • 事务持续时间分布
  • 回滚操作的频率

这些数据对于排查问题和优化系统非常有帮助。

4. 尽量避免跨数据中心事务

如果你的服务分布在多个地域或 IDC,那么分布式事务的成本会急剧上升,这时候应该考虑用“本地事务 + 最终一致性”这种策略,而不是死磕分布式事务本身。


写在最后:技术是手段,业务才是目的

回想起那段日夜赶工的日子,说实话挺累的。但最让我感慨的是,技术本身并不是终点,它只是达成业务目标的一种手段。

我们花了很多精力去解决分布式事务的问题,归根到底还是为了让用户能顺利下单、商家能准确发货、公司能稳定营收。

而一个好的架构师/工程师,不只是懂技术,更重要的是懂得取舍,知道什么时候要用分布式事务、什么时候可以用异步解耦、什么时候干脆就不做一致性处理。

希望这篇来自真实项目的经验之谈,能帮你绕过我踩过的那些“坑”。如果你有什么问题或想法,欢迎留言交流,我们一起成长。


祝你 coding 快乐,bug free 🙌

评论 0

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