从“拆东墙补西墙”到“最终一致”:分布式事务在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. 监控与告警:最终一致性意味着可能存在中间状态,需要定时扫描"悬挂事务",并配置告警以便人工介入。

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


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

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

相关推荐
Java编程爱好者1 小时前
Java高级面试必问:AQS 到底是什么?
后端
dgvri1 小时前
Spring Boot接收参数的19种方式
java·spring boot·后端
做个文艺程序员1 小时前
Function Calling 与工具调用:让 AI 真正干活【OpenClAW + Spring Boot 系列 第5篇】
人工智能·spring boot·后端
rOuN STAT1 小时前
Spring Boot 2.7.x 至 2.7.18 及更旧的版本,漏洞说明
java·spring boot·后端
我叫黑大帅2 小时前
Golang中的map的key可以是哪些类型?可以嵌套map吗?
后端·面试·go
青槿吖2 小时前
Feign 微服务远程调用指南:告别手写 RestTemplate
java·redis·后端·spring·微服务·云原生·架构
神奇小汤圆2 小时前
Linux 动态库 .so 工作原理,后端 / 嵌入式必看
后端
iOS妖狐小北2 小时前
RabbitMQ之交换机
分布式·rabbitmq·ruby
shy^-^cky2 小时前
RESTful 中的状态转移方法
后端·restful