分布式事务解决方案:最佳实践

日志切割师
2025-06-24 04:20
阅读 401

分布式事务实战:从踩坑到落地的经验分享

说到分布式事务,我想每个经历过微服务架构的开发者都会有那么一段“痛苦”的回忆吧。我也不例外,在我们团队的一次关键项目重构中,这个问题直接让我们在上线前卡了整整一周的时间。当时那种焦虑、无奈和后来解决后的如释重负,至今记忆犹新。

今天我就想用第一人称的方式,结合那次真实项目经历,跟你聊聊我在处理分布式事务方面的一些经验、踩过的坑,以及最终落地的解决方案。不是理论堆砌,而是真·实战经验分享,希望能对正在或即将遇到类似问题的你有所帮助。


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

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

事情要从我们公司内部的一个订单系统重构说起。

原来的系统是一个单体应用,运行在一个数据库上,所有操作都在一个事务里完成,简单粗暴但稳定。随着业务发展,我们开始拆分模块,逐步往微服务方向演进。订单中心独立出来了,库存中心、支付中心也陆续变成了独立服务,并各自拥有自己的数据库。

这时候,一个新的订单创建流程就涉及三个核心服务:

  • 订单服务:创建订单
  • 库存服务:减库存
  • 支付服务:处理支付动作

这三步操作必须满足“要么都成功,要么都失败”,否则就会出现数据不一致的问题。比如订单创建了,库存也扣了,但支付出错导致整个交易失败,这就可能造成用户账户被划款而商品没发货,进而引发投诉和纠纷。

于是——典型的分布式事务场景来了


初期方案与挑战:本地事务 + 消息补偿

初期方案与挑战:本地事务 + 消息补偿

我们最初的方案是使用“本地事务+消息队列”做异步补偿机制:

  1. 订单服务插入订单记录(本地事务)
  2. 向MQ发送一条库存扣除的消息
  3. 库存服务监听消息,执行扣减库存
  4. 后续支付服务由另一个消息触发

这样做的好处是系统解耦,性能也能撑得住大并发。但问题也很快暴露出来:

  • MQ丢了消息怎么办
  • 如果消息重复消费了,会不会多扣库存?
  • 万一订单服务崩溃了还没发消息出去呢?

我们尝试通过幂等处理+本地事务表来兜底,但在实际压测和测试环境下,依然出现了大量的状态不一致情况。比如库存已经扣完,但订单没创建成功;或者订单创建成功,库存没有及时扣掉。

这种情况下,系统需要人工介入修复数据,显然是不可接受的。


最终解决方案:引入TCC型分布式事务框架

最终解决方案:引入TCC型分布式事务框架

经过多方调研和技术选型,我们最终决定采用一种基于TCC(Try - Confirm - Cancel)模式的分布式事务框架来解决这个问题。

我们选择的是开源的 Seata 框架,它支持AT、TCC、SAGA等多种事务模式。考虑到我们已经有多个独立服务,并且数据库类型多样(MySQL、PostgreSQL都有),TCC模式更适合我们当前的情况。

TCC模式的核心思想是:

  • Try 阶段:资源预留(冻结库存、检查余额等)
  • Confirm 阶段:真正执行业务逻辑(扣库存、扣余额)
  • Cancel 阶段:回滚操作(解冻库存、恢复余额)

这种方式虽然比本地事务复杂一些,但也更灵活,尤其是在服务隔离度较高、跨系统的场景下。


实施过程:一步步搭建TCC事务链路

实施过程:一步步搭建TCC事务链路

接下来我会结合代码示例来说明我们的实现思路。注意,这部分是我根据当时的项目改写出来的简化版本,供参考。

一、定义TCC接口契约

首先我们要为各个服务定义好TCC的业务接口。以库存服务为例:

public interface InventoryService {
    
    // Try阶段:冻结库存
    boolean deductInventory(String productId, int quantity);

    // Confirm阶段:正式扣库存
    boolean confirmDeduct(String txId, String productId, int quantity);

    // Cancel阶段:回滚
    boolean cancelDeduct(String txId, String productId, int quantity);
}

这里的txId是全局事务ID,由Seata服务端统一分配。

二、集成Seata客户端配置

Spring Boot项目的application.yml配置如下:

seata:
  enabled: true
  application-id: order-service
  tx-service-group: my_test_tx_group
  service:
    vgroup-mapping:
      my_test_tx_group: default
  registry:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP

服务器部署方案-1

三、业务方法添加全局事务注解

在订单创建入口处添加全局事务控制:

@GlobalTransactional
public Order createOrder(String userId, String productId, int quantity) {

    // Step 1: 创建订单(本地事务)
    Order order = orderDao.create(userId, productId, quantity);

    try {
        // Step 2: 扣库存(TCC方式)
        inventoryService.deductInventory(productId, quantity);

        // Step 3: 扣支付(TCC方式)
        paymentService.charge(userId, productId, quantity);

        return order;

    } catch (Exception e) {
        log.error("下单失败", e);
        throw new RuntimeException("下单失败");
    }
}

这段代码看起来很简单,但实际上背后Seata会自动帮你管理事务状态,并在失败时调用Cancel方法进行回滚。

四、服务间调用加全局上下文传播

我们在Feign调用中,加入了Seata的上下文拦截器,确保每次远程调用都带上txId等事务信息:

@Configuration
public class FeignConfig {

    @Bean
    public RequestInterceptor seataRequestInterceptor() {
        return requestTemplate -> {
            String xid = RootContext.getXID();
            if (xid != null) {
                requestTemplate.header("XID", xid);
            }
        };
    }
}

API接口文档-2

接收方也需要有对应的拦截器解析这个Header并设置上下文,这样Seata才能串联起整个事务链。


踩坑经验:那些让我头大的事

再牛的框架,用不好照样翻车。我们在实践中踩了不少坑,下面挑几个比较重要的说说。

坑点1:Cancel方法幂等问题没处理好

刚开始我们写的Cancel方法没考虑幂等性,结果出现过多次Cancel被重复调用,导致库存变成负数。后来我们加上了一个事务ID + 状态标志位的方式:

@Override
public boolean cancelDeduct(String txId, String productId, int quantity) {
    if (txLogService.isTxProcessed(txId)) {
        return true;  // 已处理过,避免重复操作
    }

    // 实际回滚逻辑
    inventoryDao.increase(productId, quantity);
    
    txLogService.markAsProcessed(txId);
    return true;
}

坑点2:事务超时时间设置不当

Seata默认的事务超时时间是60秒,但我们有个第三方支付接口响应非常慢,经常超过这个时间。后果就是Cancel被调用,钱又退回来了,但支付却最终成功了。

解决办法有两个:

  • 设置长一点的超时时间:@GlobalTransactional(timeoutMills = 300000)
  • 改成异步通知机制,避免同步阻塞等待支付结果

坑点3:生产环境Seata Server故障导致事务阻塞

我们线上部署时曾遇到一次Seata Server异常重启的情况。此时大量事务处于“未知”状态,导致部分订单无法继续执行。

我们后来做了一套定时巡检事务日志的机制,定期拉取未完成事务的状态,并主动发起Cancel或Confirm,缓解了这个问题。


实施效果与收益:不仅仅是技术提升

这套TCC事务方案上线后,我们的系统表现明显稳定了很多:

  • 数据一致性得到了保障,基本不再有人工介入修复
  • 事务追踪能力提升,出了问题能快速定位到哪一步出了异常
  • 在高并发下表现良好,QPS达到了预期目标

更重要的是,整个团队对分布式系统的设计有了更深入的理解,特别是在:

  • 服务边界设计
  • 事务划分粒度
  • 幂等性与并发控制等细节上的把控

经验总结:给开发者的几点建议

如果你也在面临分布式事务的问题,以下几点是我的真诚建议:

✅ 优先保证业务可补偿性

不是所有业务都能用强一致性保证,有些场景适合弱一致性配合异步补偿。比如统计类、非关键路径的操作,可以牺牲一点一致性,换性能和可用性。

✅ 事务粒度不宜过大

尽量把事务切小,减少锁竞争。不要把整个下单流程放到一个事务里,能拆的就拆开。比如用户身份验证可以放外面,只有核心操作走事务。

✅ 异常监控要提前做好

分布式事务一旦出问题,排查起来代价很高。务必要有一个完善的监控体系,包括但不限于:

  • 全局事务日志跟踪
  • 服务调用链分析
  • Cancel/Confirm调用次数报警

✅ 不要迷信某个框架,适合自己最重要

Seata是个不错的工具,但它不一定适合你。比如有的团队用了阿里云的GTS,也有人自研轻量级事务机制。关键是结合自己业务特点和架构水平做出选择。


结语:分布式事务没有银弹,只有权衡

写到这里,我想说的是:分布式事务从来不是一个轻松的技术话题。它的本质是在分布式系统中找到一个合理的平衡点:一致性 vs 可用性、复杂度 vs 稳定性、成本 vs 安全。

在我们这次项目落地过程中,我也深刻体会到:技术再厉害,也需要工程化思维去支撑。每一次“踩坑”,其实都是对系统设计的又一次反思和优化。

希望这篇文章能给你带来一些启发。如果你也在分布式事务这条路上挣扎过、困惑过,欢迎留言交流,我们一起成长!


📌 附录:推荐阅读资料


评论 0

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