0


Spring 事务(Transactional)失效的七种原因及解决方案(含项目代码)

Spring 事务(Transactional)失效的七种原因及解决方案(含项目代码)

简介

“Spring框架提供了强大的事务管理功能,能够确保数据库操作的一致性和可靠性。然而,有时候我们可能会遇到Spring事务失效的情况,导致数据不一致或操作失败。本文将探讨Spring事务失效的原因,以及如何避免和解决这些问题。通过深入了解失效原因,我们可以更好地利用Spring事务管理功能,确保系统的稳定性和可靠性。”

项目搭建

代码仓库URL:https://gitee.com/itwenke/spring-boot-demo/tree/master/transactional
项目截图:
在这里插入图片描述

pom配置

<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.3.1</version></dependency>

数据库配置

spring.datasource.url=jdbc:mysql://localhost:3306/spring-boot-demo?useUnicode=true&characterEncoding=UTF-8&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

数据表结构

CREATETABLE `bank_account` (
  `id` bigint NOTNULLAUTO_INCREMENT,
  `account` varchar(32)COLLATE utf8mb4_bin NOTNULLCOMMENT'账户',
  `balance` bigint NOTNULLCOMMENT'余额',PRIMARYKEY(`id`))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='银行账户';

失效原因

私有方法private

Spring的事务代理通常是通过Java动态代理或CGLIB动态代理生成的,这些代理要求目标方法是公开可访问的(public)。私有方法无法被代理,因此事务将无效。

@Transactional(rollbackFor =Exception.class)privatevoidaddBankAccount(BankAccount bankAccount)throwsException{
    bankAccountService.addBankAccount(bankAccount);thrownewException("测试事务回滚");}

直接使用时,这种场景也不太容易出现,因为IDEA会有提醒。
解决方法是将目标方法改为public或protected。

@Transactional(rollbackFor =Exception.class)publicvoidaddBankAccount(BankAccount bankAccount)throwsException{
    bankAccountService.addBankAccount(bankAccount);thrownewException("测试事务回滚");}

目标类没有配置为Bean

Spring的事务管理需要在Spring容器中配置的Bean上才能生效。如果目标类没有被配置为Spring Bean,那么事务将无法被应用。

publicclassNonBeanDemo{@Transactional(rollbackFor =Exception.class)publicvoidaddBankAccount(BankAccount bankAccount)throwsException{IBankAccountService bankAccountService =SpringBeanUtil.getBean(IBankAccountService.class);
        bankAccountService.addBankAccount(bankAccount);thrownewException("测试事务回滚");}}

解决方法是确保目标类被正确配置为Spring Bean。

@ComponentpublicclassNonBeanDemo{@Transactional(rollbackFor =Exception.class)publicvoidaddBankAccount(BankAccount bankAccount)throwsException{IBankAccountService bankAccountService =SpringBeanUtil.getBean(IBankAccountService.class);
        bankAccountService.addBankAccount(bankAccount);thrownewException("测试事务回滚");}}

异常不匹配

@Transactional注解默认处理运行时异常,即只有抛出运行时异常,才会触发事务回滚。

@TransactionalpublicvoidaddBankAccount(BankAccount bankAccount)throwsException{
    bankAccountService.addBankAccount(bankAccount);thrownewException("测试事务回滚");}

解决方法是@Transactional设置为@Transactional(rollbackFor = Exception.class)。

@Transactional(rollbackFor =Exception.class)publicvoidaddBankAccount(BankAccount bankAccount)throwsException{
    bankAccountService.addBankAccount(bankAccount);thrownewException("测试事务回滚");}

跨越多个线程

如果您的应用程序在多个线程之间共享数据库连接和事务上下文,事务可能会失效,除非适当地配置事务传播属性。

  1. 子线程抛异常,主线程正常:
@Transactional(rollbackFor =Exception.class)publicvoidaddBankAccount1(){newThread(()->{BankAccount bankAccount =newBankAccount();
        bankAccount.setAccount("11111111");
        bankAccount.setBalance(10000L);
        bankAccountService.addBankAccount(bankAccount);}).start();newThread(()->{BankAccount bankAccount =newBankAccount();
        bankAccount.setAccount("22222222");
        bankAccount.setBalance(10000L);
        bankAccountService.addBankAccount(bankAccount);thrownewRuntimeException("测试事务回滚");}).start();}
  1. 主线程抛异常,子线程正常:
@Transactional(rollbackFor =Exception.class)publicvoidaddBankAccount2(){newThread(()->{BankAccount bankAccount =newBankAccount();
        bankAccount.setAccount("11111111");
        bankAccount.setBalance(10000L);
        bankAccountService.addBankAccount(bankAccount);}).start();newThread(()->{BankAccount bankAccount =newBankAccount();
        bankAccount.setAccount("22222222");
        bankAccount.setBalance(10000L);
        bankAccountService.addBankAccount(bankAccount);}).start();thrownewRuntimeException("测试事务回滚");}

解决方法:参考分布式事务2PC(二阶段提交)方案,2PC是同步阻塞协议,需要等待各个线程执行完成才能进行”提交“还是”回滚”的操作。
在这里插入图片描述

publicclassMultiThreadingTransactionManager{/**
     * 事务管理器
     */privatefinalPlatformTransactionManager platformTransactionManager;/**
     * 超时时间
     */privatefinallong timeout;/**
     * 时间单位
     */privatefinalTimeUnit unit;/**
     * 主线程门闩:当所有的子线程准备完成时,通知主线程判断统一”提交“还是”回滚”
     */privatefinalCountDownLatch mainStageLatch =newCountDownLatch(1);/**
     * 子线程门闩:count 为0时,说明子线程都已准备完成了
     */privateCountDownLatch childStageLatch =null;/**
     * 是否提交事务
     */privatefinalAtomicBoolean isSubmit =newAtomicBoolean(true);/**
     * 构造方法
     *
     * @param platformTransactionManager 事务管理器
     * @param timeout 超时时间
     * @param unit 时间单位
     */publicMultiThreadingTransactionManager(PlatformTransactionManager platformTransactionManager,long timeout,TimeUnit unit){this.platformTransactionManager = platformTransactionManager;this.timeout = timeout;this.unit = unit;}/**
     * 任务执行器
     *
     * @param tasks 任务列表
     * @param executorService 线程池
     * @return 是否执行成功
     */publicbooleanexecute(List<Runnable> tasks,ThreadPoolTaskExecutor executorService){// 排查null空值
        tasks.removeAll(Collections.singleton(null));// 属性初始化init(tasks.size());for(Runnable task : tasks){// 创建线程Thread thread =newThread(()->{// 判断其它线程是否已经执行任务失败,失败就不执行了if(!isSubmit.get()){
                    childStageLatch.countDown();}// 开启事务DefaultTransactionDefinition defaultTransactionDefinition =newDefaultTransactionDefinition();TransactionStatus transactionStatus = platformTransactionManager.getTransaction(defaultTransactionDefinition);try{// 执行任务
                    task.run();}catch(Exception e){// 任务执行失败,设置回滚
                    isSubmit.set(false);}// 计数器减一
                childStageLatch.countDown();try{// 等待主线程的指示,判断统一”提交“还是”回滚”
                    mainStageLatch.await();if(isSubmit.get()){// 提交
                        platformTransactionManager.commit(transactionStatus);}else{// 回滚
                        platformTransactionManager.rollback(transactionStatus);}}catch(InterruptedException e){
                    e.printStackTrace();}});// 线程池执行任务
            executorService.execute(thread);}try{// 主线程等待所有子线程准备完成,避免死锁,设置超时时间
            childStageLatch.await(timeout, unit);long count = childStageLatch.getCount();// 主线程等待超时,子线程可能发生长时间阻塞,死锁if(count >0){// 设置回滚
                isSubmit.set(false);}// 主线程通知子线程”提交“还是”回滚”
            mainStageLatch.countDown();}catch(InterruptedException e){
            e.printStackTrace();}// 返回执行结果是否成功return isSubmit.get();}/**
     * 属性初始化
     * @param size 任务数量
     */privatevoidinit(int size){
        childStageLatch =newCountDownLatch(size);}}

注意事项1: 2PC是同步阻塞协议,各个任务会等待所有的任务完成准备阶段才能进一步执行,所以在使用中一定要给任务列表提供充足的空闲线程,比如任务列表长度为8,线程池最大线程数不能小于8,否则会使其中的几个任务得不到执行,而其他线程会一直进行等待。即使有一阶段超时处理,事务也始终得不到提交。

注意事项2: 如果你的任务是对数据库进行操作,需要考虑数据库连接是否充足,线程等待过程中不会释放数据库连接,如果Connection不够,即使任务被线程池调度执行,也会阻塞在获取数据库连接中,同样会发生“死锁”。

事务传播属性

事务传播属性定义了事务如何传播到嵌套方法或外部方法。如果事务传播属性设置不正确,可能会导致事务失效或不符合预期的行为。
以下是七种事务传播类型:

  1. REQUIRED: 如果当前存在事务,则加入该事务,如果当前没有事务,则创建一个新的事务。这是最常用的传播行为,也是默认的,适用于大多数情况。(默认事务:有就加入,没有就新建)
  2. REQUIRES_NEW: 无论当前是否存在事务,都创建一个新的事务。如果当前存在事务,则将当前事务挂起。适用于需要独立事务执行的场景,不受外部事务的影响。(独立事务:有没有,都新建)
  3. SUPPORTS: 如果当前存在事务,则加入该事务,如果当前没有事务,则以非事务方式执行。适用于不需要强制事务的场景,可以与其他事务方法共享事务。(不强制事务:有就加入,没有就没有)
  4. NOT_SUPPORTED: 以非事务方式执行,如果当前存在事务,则将当前事务挂起。适用于不需要事务支持的场景,可以在方法执行期间暂时禁用事务。(非事务:有不加入,没有也不新建)
  5. MANDATORY: 如果当前存在事务,则加入该事务,如果当前没有事务,则抛出异常。适用于必须在事务中执行的场景,如果没有事务则会抛出异常。(强制事务:有就加入,没有就抛异常)
  6. NESTED: 如果当前存在事务,则在嵌套事务中执行,如果当前没有事务,则创建一个新的事务。嵌套事务是外部事务的一部分,可以独立提交或回滚。适用于需要在嵌套事务中执行的场景。(嵌套事务:有就嵌套,没有就新建)
  7. NEVER: 以非事务方式执行,如果当前存在事务,则抛出异常。适用于不允许在事务中执行的场景,如果存在事务则会抛出异常。(强制非事务:没有就没有,有就抛异常)

使用CGLIB动态代理

默认情况下,Spring的事务代理使用基于接口的JDK动态代理。如果您将@Transactional注解声明在接口上,而目标类是使用CGLIB代理的,事务将不会生效。

解决方法是将@Transactional注解移到目标类的方法上,或者配置Spring以使用CGLIB代理接口。

内部类访问

类内部非直接访问带注解标记的方法addBankAccount,而是通过类普通方法 testInnerClass,然后由 testInnerClass 调用 addBankAccount。

@Transactional(rollbackFor =Exception.class)publicvoidaddBankAccount(BankAccount bankAccount)throwsException{
    bankAccountService.addBankAccount(bankAccount);thrownewException("测试事务回滚");}publicvoidtestInnerClass(BankAccount bankAccount)throwsException{addBankAccount(bankAccount);}

解决方法是使用SpringBeanUtil.getBean()获取代理对象。

@Transactional(rollbackFor =Exception.class)publicvoidaddBankAccount(BankAccount bankAccount)throwsException{
    bankAccountService.addBankAccount(bankAccount);thrownewException("测试事务回滚");}publicvoidtestInnerClass(BankAccount bankAccount)throwsException{InnerClassDemo innerClassDemo =SpringBeanUtil.getBean(InnerClassDemo.class);
    innerClassDemo.addBankAccount(bankAccount);}
标签: spring java 后端

本文转载自: https://blog.csdn.net/qq_45607784/article/details/134897741
版权归原作者 itwenke 所有, 如有侵权,请联系我们删除。

“Spring 事务(Transactional)失效的七种原因及解决方案(含项目代码)”的评论:

还没有评论