本地事务实效-分布式架构

本地事务失效 :本以为在同一个事务内的操作,实际上并没有被一起提交或回滚。通常发生在分布式或复杂应用架构中。
核心原因:本地数据库事务(通常指单个数据库上连接的事务)的有效范围,无法覆盖到所有想要一起提交或回滚的操作。

本地事务失效的典型场景

1.跨多个数据源(多个数据库连接)

场景描述 :需要同时操作两个独立的数据库,例如DB1和DB2。
代码示例

java 复制代码
@Transactional
public void transferAcrossDatabase() {
    // 操作一:从DB1的用户表扣钱
    userMapper.deductMoney("userA", 100); // 使用DataSource1连接DB1
    
    // 操作二:往DB2的账户表加钱
    accountMapper.addMoney("userB", 100); // 使用DataSource2连接DB2
}

失效原因

  • 这两个Mapper通常使用不同的DataSource,即两个不同的数据库连接。
  • @Transactional默认只能管理一个数据库连接上的事务。
  • 当deductMoney执行成功后,它的连接会立即提交事务(如果后续代码没出错),而addMoney是在另一个连接上执行,属于另一个独立的事务。此时如果addMoney失败,deductMoney的操作并不会回滚。

2.跨服务方法调用(RPC/HTTP)

在微服务架构中,这是导致数据不一致的"头号杀手"。
场景描述 :在同一个服务内,一个事务方法调用了另一个服务的方法。
代码示例

java 复制代码
@Transactional
public void placeOrder() {
    // 1. 本地数据库操作:创建订单
    orderMapper.insert(order); // 本地事务管理
    
    // 2. 远程服务调用:扣减库存
    inventoryServiceClient.deductStock(productId, quantity); // HTTP或RPC调用
    // 3. 后续本地代码
    ...
}

失效原因

  • 库存服务是另一个独立的进程,有自己独立的数据库。
  • 本地事务只能回滚orderMapper.insert的操作。
  • 如果deductStock调用失败,虽然可以抛出异常回滚导致本地订单创建,但如果deductStock调用成功,而后续本地代码出错,会导致订单回滚,但库存已经被扣减的"脏数据"。

3.非公共方法或方法内调用

这与Spring AOP(面向切面编程)的实现机制有关。
场景描述 :在同一个类中,一个方法调用另一个有@Transactional注解的方法。
代码示例

java 复制代码
public class OrderService {
    
    public void createOrder() {
        // ... 一些逻辑
        this.insertOrder(); // 在类内部调用事务方法
    }
    
    @Transactional
    public void insertOrder() {
        orderMapper.insert(order);
        // 其他数据库操作
    }
}

失效原因

  • Spring的事务管理是通过AOP代理实现的。当从类外部调用insertOrder时,实际上调用的是Spring生成的代理对象的方法,代理对象会开启事务。
  • 当在类内部(如createOrder中)调用this.insertOrder()时,这是目标对象自身的调用,绕过了代理,因此@Transactional注解不会生效。

4.异常被捕获或被"吃掉"

事务的回滚依赖于运行时异常(RuntimeException)和Error。如果异常处理方式不对,事务管理器就不知道需要回滚。
场景描述 :在事务方法中,你把可能抛出异常的地方用try-catch包起来,但没有在catch块中再次抛出异常。
代码示例

java 复制代码
@Transactional
public void updateUser() {
    try {
        userMapper.update(user); // 如果这里出错...
        // ... 其他操作
    } catch (Exception e) {
        logger.error("更新用户失败", e);
        // 只是记录了日志,没有抛出新异常!
        // 事务管理器认为一切正常,会提交事务。
    }
}

失效原因:事务管理器只有在接收到异常信号时才会触发回滚。吞掉异常,就等于告诉框架"一切正常,可以提交"。

5.错误的异常类型

默认情况下,Spring事务只对未检查的异常(即RuntimeException和Error)进行回滚。对已检查异常(如Exception,IOException,SQLException等)不回滚。
场景描述 :方法抛出了一个已检查异常。
代码示例

java 复制代码
@Transactional
public void updateUser() throws Exception { // 声明抛出已检查异常
    userMapper.update(user);
    if (someCondition) {
        throw new Exception("一个业务异常"); // 抛出已检查异常
    }
}

失效原因:即使抛出了异常,但因为它是Exception类型,Spring默认不会回滚事务。

6.手动切断了数据库连接

在一些特殊操作中,可能会手动提交或设置自动提交,干扰了Spring的事务管理。
场景描述 :在事务方法中,手动获取了连接并改变了其自动提交状态。
代码示例

java 复制代码
@Transactional
public void manualConnectionOperation() {
    SomeData data = getFromDatabase();
    // 手动获取连接并操作
    Connection conn = DataSourceUtils.getConnection(dataSource);
    conn.setAutoCommit(true); // 改为自动提交,破坏了事务
    // ... 执行一些SQL,会立即提交
    conn.setAutoCommit(false);
}

失效原因:Spring通过将autoCommit设置为false来管理事务。手动改为了true,会导致SQL语句立即提交,不受事务控制。

如果避免和解决?

  1. 对于场景1(多数据源):使用分布式事务解决方案,如基于XA协议的JTA(如Atomikos)、Seata等。
  2. 对于场景2(跨服务):采用最终一致性方案,如Saga模式、事务消息(RocketMQ)、TCC模式等。
  3. 对于场景3(方法内调用):
    • 将事务方法移到另一个Bean中。
    • 通过AopContext.currentProxy()获取代理对象再调用。
  4. 对于场景4和5(异常处理):
    • 确保在catch块中抛出运行时异常,例如throw new RuntimeException(e);。
    • 使用@Transactional(rollbackFor = Exception.class)注解,强制对所有Exception及其子类都进行回滚。
  5. 对于场景6(手动连接):避免在事务方法中手动操作连接和提交,交由Spring统一管理。

总结

理解本地事务失效的关键在于理解它的边界------它只能管住当前方法通过同一个数据库连接执行的SQL操作。一旦你的操作超出了这个边界(跨连接、跨进程、跨方法调用),本地事务就会失效。

相关推荐
打码人的日常分享2 小时前
基于信创体系政务服务信息化建设方案(PPT)
大数据·服务器·人工智能·信息可视化·架构·政务
T***u3332 小时前
SpringBoot集成SkyWalking,分布式链路追踪
spring boot·分布式·skywalking
小坏讲微服务2 小时前
Spring Cloud Alibaba整合SkyWalking的监控完整使用
java·微服务·架构·springcloud·监控·skywalking·java微服务
Bohemian—Rhapsody2 小时前
kafka主题(topic)数据保留时间设置
分布式·kafka
回家路上绕了弯4 小时前
订单超时自动取消:从业务场景到技术落地的完整设计方案
分布式·后端
周杰伦_Jay4 小时前
【基于 Spring Cloud Alibaba 的微服务电商项目】完整实现思路
微服务·云原生·架构
7***53344 小时前
微服务分布式事务解决方案
分布式·微服务·架构
S***q1924 小时前
后端服务架构设计:从单体到微服务
java·微服务·架构
T***u3334 小时前
微服务书籍
java·微服务·架构