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

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

本地事务失效的典型场景

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操作。一旦你的操作超出了这个边界(跨连接、跨进程、跨方法调用),本地事务就会失效。

相关推荐
周壮8 小时前
01 一探究竟:从架构的演变看微服务化架构
微服务·云原生·架构
周壮8 小时前
04 服务治理:Nacos 如何实现微服务服务治理
微服务·云原生·架构
小程故事多_809 小时前
攻克RAG系统最后一公里 图文混排PDF解析的挑战与实战方案
人工智能·架构·pdf·aigc
王然-HUDDM11 小时前
HUDDM:首个基于认知结构的AI系统设计的AI模型
功能测试·神经网络·架构·系统架构·agi
2301_8153577011 小时前
Java项目架构从单体架构到微服务架构的发展演变
java·微服务·架构
代码游侠14 小时前
复习——ARM Cortex-A 裸机开发深度解析
arm开发·笔记·嵌入式硬件·学习·架构
敏叔V58714 小时前
联邦学习与大模型:隐私保护下的分布式模型训练与微调方案
分布式
努力搬砖的咸鱼15 小时前
Kubernetes 核心对象详解:Pod、Deployment、Service
微服务·云原生·容器·架构·kubernetes
套码汉子15 小时前
从 “重复造轮子” 到 “搭积木式开发”:活动系统架构如何支撑业务高效迭代
架构·系统架构·游戏开发·组件化
短剑重铸之日16 小时前
《7天学会Redis》特别篇: Redis分布式锁
java·redis·分布式·后端·缓存·redission·看门狗机制