spring事务失效的几种业务场景

Spring 事务失效的核心原因是 AOP 动态代理未触发事务配置不合法异常未被感知底层不支持事务。以下是最常见的失效场景:

一、最常见:内部方法调用(同一类内无代理转发)

场景描述

同一个类中,无事务的方法 A 调用有事务的方法 B,此时方法 B 的事务会失效。

失效原因

Spring 事务基于动态代理(JDK/CGLIB),只有通过 代理对象 调用目标方法时,AOP 切面才会触发事务逻辑。而同一类内的方法调用是 this.方法()(目标对象自身调用),跳过了代理对象,事务切面无法介入。

示例代码

typescript 复制代码
@Service
public class UserService {

    // 无事务方法
    public void updateUserWithoutTx() {
        // 内部调用有事务方法(this 调用,无代理)
        this.updateUserWithTx(); // 事务失效
    }

    // 有事务方法
    @Transactional
    public void updateUserWithTx() {
        // 数据库操作(无事务支持)
        userMapper.update();
    }
}

解决方案

  1. 拆分方法到不同类(通过依赖注入调用代理对象);
  2. 自注入代理对象(@Autowired private UserService thisProxy;,需开启 exposeProxy = true);
  3. 通过 AopContext.currentProxy() 获取代理对象调用。

二、方法访问权限非 public

场景描述

@Transactional 注解加在 非 public 方法(private/protected/default)上。

失效原因

Spring 事务拦截器(TransactionInterceptor)的源码中,会判断目标方法的访问权限:仅 public 方法会触发事务 。非 public 方法的 Method.getModifiers() 不满足 Modifier.isPublic(modifiers),事务切面直接跳过。

示例代码

typescript 复制代码
@Service
public class UserService {

    // 错误:private 方法加事务(失效)
    @Transactional
    private void updateUser() {
        userMapper.update();
    }
}

解决方案

将事务方法改为 public 访问权限。

三、异常被手动捕获(未抛出到代理层)

场景描述

事务方法内部捕获了异常(try-catch),且未重新抛出异常。

失效原因

Spring 事务默认通过 感知未捕获的异常 来触发回滚(RuntimeException/Error)。如果异常被手动捕获且未抛出,代理层无法感知异常,会认为方法执行成功,直接提交事务。

示例代码

java 复制代码
@Service
public class UserService {

    @Transactional
    public void updateUser() {
        try {
            userMapper.update();
            int i = 1 / 0; // 抛出 RuntimeException
        } catch (Exception e) {
            // 错误:捕获异常但未抛出,事务无法感知
            log.error("更新失败", e);
        }
    }
}

解决方案

  1. 捕获后重新抛出异常(throw new RuntimeException(e));
  2. 手动触发回滚(TransactionAspectSupport.currentTransactionStatus().setRollbackOnly())。

四、抛出非检查异常(默认不回滚的异常)

场景描述

事务方法抛出 检查异常 (如 IOExceptionSQLException),且未通过 rollbackFor 配置指定回滚异常。

失效原因

Spring 事务默认只回滚 RuntimeExceptionError(非检查异常)。对于检查异常(编译时必须捕获的异常),默认不会触发回滚,事务会正常提交。

示例代码

java 复制代码
@Service
public class UserService {

    // 错误:抛出 IOException(检查异常),默认不回滚
    @Transactional
    public void updateUser() throws IOException {
        userMapper.update();
        throw new IOException("文件读取失败");
    }
}

解决方案

通过 rollbackFor 显式指定回滚异常:

java 复制代码
@Transactional(rollbackFor = Exception.class) // 捕获所有异常回滚
public void updateUser() throws IOException {
    // ...
}

五、目标类 / 方法为 final(无法生成代理)

场景描述

  1. 事务所在类是 final 修饰的;
  2. 事务方法是 final 修饰的。

失效原因

Spring 动态代理的两种实现:

  • JDK 动态代理:基于接口,目标类需实现接口(final 类无法被代理);
  • CGLIB 动态代理:基于类继承,final 类无法被继承,final 方法无法被重写。

无论哪种方式,final 修饰都会导致代理对象无法生成或无法重写目标方法,事务切面无法切入。

示例代码

java 复制代码
// 错误:final 类(无法生成 CGLIB 代理)
@Service
public final class UserService {

    // 错误:final 方法(无法被 CGLIB 重写)
    @Transactional
    public final void updateUser() {
        userMapper.update();
    }
}

解决方案

移除 final 修饰符(类和事务方法均不能为 final)。

六、Bean 未被 Spring 容器管理

场景描述

事务所在的类未通过 @Service@Component 等注解交给 Spring 管理,而是手动 new 出来的对象。

失效原因

Spring 事务依赖容器生成的 代理 Bean ,手动 new 的对象是原始目标对象,未被 Spring 增强(无代理),自然没有事务支持。

示例代码

java 复制代码
// 错误:未加 @Service,未被 Spring 管理
public class UserService {

    @Transactional
    public void updateUser() {
        userMapper.update();
    }
}

// 调用时手动 new(无事务)
UserService userService = new UserService();
userService.updateUser();

解决方案

  1. 给类加 @Service 注解,通过 @Autowired 注入使用;
  2. 确保对象是从 Spring 容器中获取的(而非手动实例化)。

七、事务管理器配置错误 / 缺失

场景描述

  1. 未配置 PlatformTransactionManager Bean;
  2. 多数据源时,事务管理器指定错误;
  3. 未开启事务支持(非 Spring Boot 项目)。

失效原因

Spring 事务需要 PlatformTransactionManager(如 DataSourceTransactionManager)来管理事务生命周期(开启 / 提交 / 回滚)。无配置或配置错误时,事务注解无法生效。

常见错误场景

  1. 非 Spring Boot 项目未加 @EnableTransactionManagement 注解;
  2. 多数据源时,未通过 @Transactional(transactionManager = "xxxTxManager") 指定对应的事务管理器;
  3. 数据源未注入事务管理器(如 DataSourceTransactionManager 未传入 dataSource)。

解决方案

  1. Spring Boot 项目:引入 spring-boot-starter-jdbcspring-boot-starter-data-jpa,自动配置 DataSourceTransactionManager
  2. 非 Spring Boot 项目:添加 @EnableTransactionManagement,并手动配置事务管理器:
less 复制代码
@Configuration
@EnableTransactionManagement
public class TxConfig {
    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

八、事务传播机制配置不当

场景描述

事务方法的传播机制配置为 不支持事务 的类型,如 NOT_SUPPORTEDNEVER

失效原因

Spring 事务传播机制定义了方法间调用的事务行为,以下传播机制会导致事务失效:

  • Propagation.NOT_SUPPORTED:以非事务方式执行,若当前存在事务则挂起;
  • Propagation.NEVER:以非事务方式执行,若当前存在事务则抛出异常;
  • Propagation.SUPPORTS:若当前无事务,则以非事务方式执行(仅依赖外层事务)。

示例代码

java 复制代码
@Service
public class UserService {

    // 错误:传播机制为 NOT_SUPPORTED(不支持事务)
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void updateUser() {
        userMapper.update();
    }
}

解决方案

使用支持事务的传播机制(默认 Propagation.REQUIRED 即可):

java 复制代码
@Transactional(propagation = Propagation.REQUIRED) // 默认值,无需显式配置
public void updateUser() {
    // ...
}

九、数据库本身不支持事务

场景描述

使用的数据库 / 表引擎不支持事务(如 MySQL 的 MyISAM 引擎)。

失效原因

Spring 事务是 基于数据库事务的上层封装,若底层数据库不支持事务,即使 Spring 配置正确,也无法实现事务特性。

常见情况

  • MySQL 表引擎为 MyISAM(默认 InnoDB 支持事务);
  • 部分内存数据库(如 H2 非事务模式)。

解决方案

将数据库表引擎改为支持事务的类型(如 MySQL 改为 InnoDB)。

十、多线程调用(事务上下文不共享)

场景描述

事务方法 A 中启动新线程,调用事务方法 B,方法 B 的事务与 A 独立,且 A 的异常不会导致 B 回滚。

失效原因

Spring 事务上下文(TransactionSynchronizationManager)是 线程局部变量(ThreadLocal) ,线程间不共享。新线程中调用的事务方法会创建独立的事务,与主线程事务无关。

示例代码

typescript 复制代码
@Service
public class UserService {

    @Transactional
    public void updateUserA() {
        userMapper.updateA();
        // 新线程调用事务方法 B
        new Thread(() -> updateUserB()).start();
        int i = 1 / 0; // 主线程抛出异常,A 回滚,但 B 已提交
    }

    @Transactional
    public void updateUserB() {
        userMapper.updateB();
    }
}

若需多线程事务一致性,需使用 分布式事务框架(如 Seata、Atomikos),或通过消息队列实现最终一致性。

总结:事务失效的核心排查思路

  1. 检查方法是否是 public,类 / 方法是否为 final
  2. 检查是否是同一类内的内部调用(this 调用);
  3. 检查异常是否被捕获未抛出,或是否是未配置 rollbackFor 的检查异常;
  4. 检查 Bean 是否被 Spring 管理(是否有 @Service 等注解,是否通过 @Autowired 注入);
  5. 检查事务管理器是否配置正确(是否有 PlatformTransactionManager Bean,多数据源是否指定事务管理器);
  6. 检查数据库 / 表是否支持事务(如 MySQL 引擎是否为 InnoDB)。
相关推荐
王中阳Go7 小时前
面试被挂的第3次,面试官说:你懂的LLM框架,只够骗骗自己
面试·职场和发展
terminal0078 小时前
浅谈useRef的使用和渲染机制
前端·react.js·面试
Dream it possible!10 小时前
LeetCode 面试经典 150_二叉树层次遍历_二叉树的层序遍历(83_102_C++_中等)
c++·leetcode·面试·二叉树
南山小乌贼12 小时前
集成电路综合总结面试宝典十五
面试·职场和发展·硬件工程师·pcb·硬件测试·硬件面试·集成电路面试
云泽80813 小时前
攻克算法面试:C++ Vector 核心问题精讲
c++·算法·面试
许强0xq13 小时前
Q10: 对于地址白名单,使用 mapping 还是 array 更好?为什么?
面试·职场和发展·web3·solidity·evm·gas
吃饺子不吃馅14 小时前
react-grid-layout 原理拆解:布局引擎、拖拽系统与响应式设计
前端·面试·架构
敲敲了个代码15 小时前
React组件命名为什么用小写开头会无法运行?
前端·javascript·react.js·面试·职场和发展·前端框架
知其然亦知其所以然15 小时前
读这一篇,你能把哨兵模式讲给 HR 听她都能懂!
redis·后端·面试