从“拆东墙补西墙”到“最终一致”:分布式事务在Spring Boot/Cloud中的破局之道

摘要:在微服务架构下,如何保证跨服务的数据一致性?是选 Seata 还是 RocketMQ?TCC 模式真的那么难写吗?本文带你从零理解分布式事务核心原理,并手把手在 Spring Boot 中落地三种主流解决方案。


🧐 一、为什么我们需要分布式事务?

先来看一个经典的 "下单扣库存" 场景:

  1. 订单服务:创建订单
  2. 库存服务:扣减库存
  3. 账户服务:扣减余额

在单体应用中,一个 @Transactional 注解就能保证 ACID,要么全成功,要么全回滚。

但在微服务架构下,三个服务连接着三个独立的数据库。当订单创建成功,但库存扣减失败时,就会出现 "订单已生成但库存未减少" 的严重数据不一致问题。

分布式事务,就是为了解决这种跨多个独立数据节点(数据库、消息队列、缓存等)的事务一致性问题而生的技术方案。


📚 二、理论基础:从 ACID 到 CAP 与 BASE

在深入代码之前,我们需要明确两个核心概念:

2.1 本地事务的 ACID

  • Atomicity:原子性
  • Consistency:一致性
  • Isolation:隔离性
  • Durability:持久性

2.2 分布式环境下的权衡

分布式系统无法同时满足 CAP 定理中的三点,由于分区容错性(P)是必然属性,我们通常只能在一致性(C)和可用性(A)之间做权衡。

这催生了 BASE 理论

  • Basically Available(基本可用)
  • Soft state(软状态)
  • Eventually consistent(最终一致性)

核心结论 :大多数分布式事务方案不再追求强一致性(实时一致),而是转向最终一致性


⚖️ 三、主流方案大比拼:谁是你的菜?

目前国内企业应用最广的是 Seata AT模式RocketMQ事务消息。以下是详细对比:

方案 原理 一致性 性能 复杂度 适用场景
XA / 2PC 两阶段提交 强一致 银行、金融等对一致性要求极高的场景
TCC Try-Confirm-Cancel 强一致 核心交易、高并发场景
可靠消息 本地事务+MQ 最终一致 异步解耦、对实时性要求不高的场景
Saga 长事务+补偿 最终一致 长流程、跨多服务的事务
Seata AT 自动补偿(数据快照) 强一致 希望无侵入改造的现有项目

💻 四、Spring Boot/Cloud 实战:三种方案代码实现

技术栈:Spring Boot 2.7.x + Spring Cloud Alibaba 2021.x + Seata 1.6.1 + RocketMQ 4.9.x + Nacos

4.1 方案一:Seata AT模式(无侵入首选)

Seata AT模式通过代理数据源,自动生成回滚SQL,业务代码几乎零改造

步骤 1:引入依赖

xml 复制代码
<!-- seata spring cloud starter -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>

步骤 2:配置 application.yml

yaml 复制代码
seata:
  enabled: true
  application-id: ${spring.application.name}
  tx-service-group: my_test_tx_group # 事务组名称,需与Seata Server一致
  registry:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848

步骤 3:全局发起方添加 @GlobalTransactional

这是最关键的一步,只需在入口方法添加注解:

java 复制代码
@Service
@Slf4j
public class OrderService {

    @GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
    public void createOrder(OrderDTO orderDTO) {
        // 1. 本地创建订单
        orderMapper.insert(order);
        
        // 2. 远程调用库存服务(Feign)
        inventoryFeign.deduct(orderDTO.getProductId(), orderDTO.getQuantity());
        
        // 3. 远程调用账户服务(Feign)
        accountFeign.debit(orderDTO.getUserId(), orderDTO.getAmount());
        
        // 4. 更新订单状态
        orderMapper.updateById(order);
    }
}

效果:当任何一步异常时,Seata 会自动利用 undo_log 表进行全局回滚。


4.2 方案二:RocketMQ事务消息(高吞吐、最终一致)

适用于对实时一致性要求不高,但要求高吞吐和最终一致的场景(如电商下单)。

步骤 1:发送事务消息(半消息)

java 复制代码
@Service
public class OrderService {

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    public void createOrder(OrderDTO orderDTO) {
        // 发送半消息,此时消费者不可见
        TransactionSendResult sendResult = rocketMQTemplate.sendMessageInTransaction(
            "order-topic", 
            MessageBuilder.withPayload(orderDTO).build(), 
            orderDTO
        );
    }
}

步骤 2:实现本地事务监听器

java 复制代码
@Component
@RocketMQTransactionListener(txProducerGroup = "order-producer-group")
public class OrderTransactionListener implements RocketMQLocalTransactionListener {

    // 执行本地事务
    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        try {
            // 1. 执行本地数据库操作:创建订单
            orderMapper.insert(order);
            // 2. 成功则提交消息,消费者可见
            return RocketMQLocalTransactionState.COMMIT;
        } catch (Exception e) {
            // 3. 失败则回滚消息
            return RocketMQLocalTransactionState.ROLLBACK;
        }
    }

    // 事务状态回查(防止网络抖动导致状态未知)
    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
        // 根据业务ID查询订单是否存在
        return orderMapper.exists(...) ? RocketMQLocalTransactionState.COMMIT : RocketMQLocalTransactionState.ROLLBACK;
    }
}

⚠️ 注意 :消费方(库存/账户服务)必须做幂等设计,因为MQ可能会重发消息。


4.3 方案三:TCC模式(高性能、手动补偿)

TCC 适用于高一致性要求且需要精准控制资源的场景,但开发量较大。

步骤 1:定义 TCC 接口

java 复制代码
@LocalTCC
public interface AccountTccAction {

    // Try阶段:资源检查与冻结
    @TwoPhaseBusinessAction(name = "debit", commitMethod = "commit", rollbackMethod = "rollback")
    boolean debit(BusinessActionContext context, @BusinessActionContextParameter(paramName = "userId") Long userId, @BusinessActionContextParameter(paramName = "amount") BigDecimal amount);

    // Confirm阶段:确认执行(扣减冻结金额)
    boolean commit(BusinessActionContext context);

    // Cancel阶段:取消执行(解冻金额)
    boolean rollback(BusinessActionContext context);
}

步骤 2:实现逻辑

  • Try:冻结资金(如:余额100 -> 可用80,冻结20)。
  • Confirm:扣除冻结资金(如:冻结20 -> 0)。
  • Cancel:解冻资金(如:冻结20 -> 0,可用80 -> 100)。

难点 :TCC 需要处理幂等空回滚 (Try没执行直接Cancel)和悬挂问题,建议直接使用 Seata 框架提供的支持。


💡 五、方案选型建议

场景描述 推荐方案 理由
金融支付、核心交易 Seata AT / XA 要求强一致性,且并发量通常可控
高并发、短事务 RocketMQ事务消息 允许短暂不一致,追求高吞吐和削峰填谷
长业务流程 Saga + 状态机 涉及多服务、多阶段,可能需要人工介入补偿
旧系统改造 Seata AT 只需代理数据源,代码侵入性最小
秒杀场景 本地事务 + 异步MQ 对一致性容忍度高,优先保证系统可用性

🛡️ 六、总结与避坑指南

分布式事务不是银弹,在落地时请务必注意以下几点:

  1. 幂等性是基石:无论用哪种方案(尤其是MQ和TCC),消费端和服务提供方都必须实现幂等,防止重复扣款或扣库存。
  2. 不要重复造轮子:尽量选择 Seata、RocketMQ 等成熟框架,不要手写底层逻辑。
  3. 超时与重试:必须配置合理的超时时间和退避策略,无限制重试可能引发雪崩。
  4. 监控与告警:最终一致性意味着可能存在中间状态,需要定时扫描"悬挂事务",并配置告警以便人工介入。

分布式事务的尽头往往不是技术,而是业务上的合理折衷。 希望本文能帮助你在项目中做出最合适的选择!


📢 关注公众号 《卷毛的技术笔记》 ,专注后端硬核技术分享,拒绝套路,只聊落地的技术。

如果觉得文章对你有帮助,欢迎点赞、收藏、关注!

相关推荐
无风听海11 小时前
ASP.NET Core .NET 10 错误响应体系全景:从 BadRequest 到编译器基础设施
后端·asp.net·.net
文心快码BaiduComate11 小时前
从个人效能到组织资产:文心快码企业版Agent Hub上线,提升团队AI编程效能
前端·后端·程序员
雪隐11 小时前
个人电脑玩AI00-前言
人工智能·后端
我是一颗柠檬12 小时前
【Java后端技术亮点】动态路由权限(按钮级权限),细粒度控制到按钮级别
java·开发语言·后端·状态模式
前端Hardy12 小时前
CSS 动画真的比 JS 快?Josh Comeau 做了组实验,结果跟直觉不一样
前端·javascript·后端
Front思12 小时前
调取支付宝支付正式环境不可以唤起来,但是沙箱可以
后端
foggyprojects12 小时前
AI 生成 SQL 模板以后,为什么还需要固定 helper 规则
后端
明天一点12 小时前
Cloudflare 通知转发钉钉机器人
前端·后端
前端Hardy12 小时前
前端日历组件,要变天了?Schedule-X v4.6 彻底杀疯了
前端·javascript·后端
Oo_行者_oO12 小时前
微服务 Feign 从“万能公共服务”到“业务客户端”
后端·架构