Spring中事务失效大致有以下八种场景

导入数据源

drop table if exists account;
create table account
(
    accountNo int primary key auto_increment,
    balance   int not null
);

insert into account (accountNo, balance)
values (1, 1000),
       (2, 1000);

1 抛出检查异常导致事务不能正确回滚

并不是任何异常情况下,spring都会回滚事务,默认情况下,RuntimeException和Error的情况下,spring事务才会回滚。

原因:

  • Spring默认只会回滚非检查异常(运行时异常和Error)

解法:

  • 配置rollbackFor属性

1.1 异常演示

 @Transactional
 public void transfer(int from, int to, int amount) throws FileNotFoundException {
      int fromBalance = accountMapper.findBalanceBy(from);
      if (fromBalance - amount >= 0) {
          accountMapper.update(from, -1 * amount);
          //异常
          new FileInputStream("aaa");
          accountMapper.update(to, amount);
      }
  }

在这里插入图片描述
可见事务并未回滚

1.2 解决办法

在这里插入图片描述

 @Transactional(rollbackFor = Exception.class)
 public void transfer(int from, int to, int amount) throws FileNotFoundException {
      int fromBalance = accountMapper.findBalanceBy(from);
      if (fromBalance - amount >= 0) {
          accountMapper.update(from, -1 * amount);
          //异常
          new FileInputStream("aaa");
          accountMapper.update(to, amount);
      }
  }

在这里插入图片描述
在这里插入图片描述

可见事务是进行了回滚的,数据库的数据也并未改变

2 业务方法内自己 try-catch 异常导致事务不能正常回滚

原因:

  • 事务通知只有捕捉到了目标异常抛出的异常,才能进行后续的回滚处理,如果目标自己处理掉异常,事务通知无法知悉

解法1:异常原样抛出

解法2:手动设置 TransactionInterceptor.currentTransactionStatus().setRollbackOnly();

1.1 异常演示

 @Transactional(rollbackFor = Exception.class)
public void transfer(int from, int to, int amount)  {
      try {
          int fromBalance = accountMapper.findBalanceBy(from);
          if (fromBalance - amount >= 0) {
              accountMapper.update(from, -1 * amount);
              new FileInputStream("aaa");
              accountMapper.update(to, amount);
          }
      } catch (FileNotFoundException e) {
          e.printStackTrace();
      }
  }

在这里插入图片描述
可见事务还是进行了提交,并未回滚

1.2 解决办法

@Transactional(rollbackFor = Exception.class)
public void transfer(int from, int to, int amount)  {
     try {
         int fromBalance = accountMapper.findBalanceBy(from);
         if (fromBalance - amount >= 0) {
             accountMapper.update(from, -1 * amount);
             new FileInputStream("aaa");
             accountMapper.update(to, amount);
         }
     } catch (FileNotFoundException e) {
         e.printStackTrace();
//            TransactionInterceptor.currentTransactionStatus().setRollbackOnly();
         throw new RuntimeException(e);
     }
 }

抛出异常或者手动设置事务状态进行回滚

在这里插入图片描述

3 aop切面顺序导致事务不能正确回滚

原因:事务切面优先级最低,但如果自定义的切面优先级和他一样,则还是自定义切面在内层,这时若自定义切面没有正确抛出异常,事务就无法回滚

解法1:异常原样抛出

解法2:手动设置 TransactionInterceptor.currentTransactionStatus().setRollbackOnly();

3.1 异常演示

在这里插入图片描述

 @Aspect
 static class MyAspect {
     @Around("execution(* transfer(..))")
     public Object around(ProceedingJoinPoint pjp) throws Throwable {
         LoggerUtils.get().debug("log:{}", pjp.getTarget());
         try {
             return pjp.proceed();
         } catch (Throwable e) {
             e.printStackTrace();
             return null;
         }
     }
 }

3.2 解决办法

抛出异常或者手动设置事务状态进行回滚
在这里插入图片描述

4 非 public 方法导致事务的失效

原因:

  • Spring为方法创建代理、添加事务通知、前提条件都是该方法是 pubic 的

解决办法:

  • 改为public方法

4.1 异常演示

 @Transactional
 void transfer(int from, int to, int amount) throws FileNotFoundException {
      int fromBalance = accountMapper.findBalanceBy(from);
      if (fromBalance - amount >= 0) {
          accountMapper.update(from, -1 * amount);
          accountMapper.update(to, amount);
      }
  }

在这里插入图片描述
无事务的开启和提交

4.2 解决办法

@Transactional
public void transfer(int from, int to, int amount) throws FileNotFoundException {
     int fromBalance = accountMapper.findBalanceBy(from);
     if (fromBalance - amount >= 0) {
         accountMapper.update(from, -1 * amount);
         accountMapper.update(to, amount);
     }
 }

在这里插入图片描述

5 父子容器导致的事务失效

原因:

  • 子容器扫描范围过大,把未加事务的配置的 service 扫描进来

解法:

  • 各扫各的,不要图简便
  • 不要用父子容器,所有bean放在同一容器(SpringBoot项目就只有一个容器,不会发生该情况

5.1 异常演示

以下是两个配置类:

@ComponentScan("day04.tx.app")
public class WebConfig {
}
@Configuration
@PropertySource("classpath:jdbc.properties")
@EnableTransactionManagement
@EnableAspectJAutoProxy(exposeProxy = true)
@ComponentScan("day04.tx.app.service")
@MapperScan("day04.tx.app.mapper")
public class AppConfig {

  @ConfigurationProperties("jdbc")
   @Bean
   public DataSource dataSource() {
       return new HikariDataSource();
   }

   @Bean
   public DataSourceInitializer dataSourceInitializer(DataSource dataSource, DatabasePopulator populator) {
       DataSourceInitializer dataSourceInitializer = new DataSourceInitializer();
       dataSourceInitializer.setDataSource(dataSource);
       dataSourceInitializer.setDatabasePopulator(populator);
       return dataSourceInitializer;
   }

   @Bean
   public DatabasePopulator databasePopulator() {
       return new ResourceDatabasePopulator(new ClassPathResource("account.sql"));
   }

   @Bean
   public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) {
       SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
       factoryBean.setDataSource(dataSource);
       return factoryBean;
   }

   @Bean
   public PlatformTransactionManager transactionManager(DataSource dataSource) {
       return new DataSourceTransactionManager(dataSource);
   }

}

然后我们加载的时候将两个配置都加载

 public static void main(String[] args) throws FileNotFoundException {
   	 GenericApplicationContext parent = new GenericApplicationContext();
     AnnotationConfigUtils.registerAnnotationConfigProcessors(parent.getDefaultListableBeanFactory());
     ConfigurationPropertiesBindingPostProcessor.register(parent.getDefaultListableBeanFactory());
     parent.registerBean(AppConfig.class);
     parent.refresh();

     GenericApplicationContext child = new GenericApplicationContext();
     AnnotationConfigUtils.registerAnnotationConfigProcessors(child.getDefaultListableBeanFactory());
     child.setParent(parent);
     child.registerBean(WebConfig.class);
     child.refresh();

     AccountController bean = child.getBean(AccountController.class);
     bean.transfer(1, 2, 500);
 }

在这里插入图片描述

5.2 解决办法

放在一个容器里面或者各扫各的

6 调用本类方法导致传播行为失效

原因:

  • 本类方法调用不经过代理,因此无法增强

解法:

  • 依赖注入自己(代理)来调用
  • 通过AopContext拿到代理对象,来调用

6.1 异常演示

@Service
public class Service6 {
    

@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
 public void foo() throws FileNotFoundException {
     LoggerUtils.get().debug("foo");
     System.out.println(this.getClass());
     bar();
 }

 @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
 public void bar() throws FileNotFoundException {
     LoggerUtils.get().debug("bar");
 }
}

在这里插入图片描述
由运行结果结果可知:

事务只开启了一次,并且打印的this的类型不为代理类

6.2 解决办法

总的来说就是要获取代理类来调用被事务增强的方法才可以保证事务不失效

@Service
public class Service6 {

    @Autowired
    private Service6 proxy;

    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public void foo() throws FileNotFoundException {
        LoggerUtils.get().debug("foo");
        System.out.println(proxy.getClass());
//        System.out.println(this.getClass());
        proxy.bar();
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
    public void bar() throws FileNotFoundException {
        LoggerUtils.get().debug("bar");
    }
}

在这里插入图片描述

由运行结果可知,事务开启了两次,并且打印的也是代理对象

另一种方法同理,只不过要在配置时加上@EnableAspectJAutoProxy(exposeProxy = true),暴露对象才可以获取

((Service6) AopContext.currentProxy()).bar();

7 @Transactional 没有保证原子行为

原因:

  • 事务的原子性仅覆盖[insert、update、delete、select] ... for update 语句,select方法并不阻塞

7.1 异常演示

CountDownLatch latch = new CountDownLatch(2);
new MyThread(() -> {
         bean.transfer(1, 2, 1000);
     latch.countDown();
 }, "t1", "boldMagenta").start();

 new MyThread(() -> {
         bean.transfer(1, 2, 1000);
     latch.countDown();
 }, "t2", "boldBlue").start();

 latch.await();
 System.out.println(bean.findBalance(1));

在这里插入图片描述
可知数据库的数据已经出现了问题

7.2 解决办法

  • synchronized 范围应扩大至代理方法调用
    在这里插入图片描述

  • 使用 select ... for update 替换 select

select balance from account where accountNo=#{accountNo} for update

8 @Transactional 方法导致的 synchronized 失效

原因:

  • synchronized保证的仅是目标方法的原子性,环绕目标方法的还有 commit 等操作,他们并不处于 sync 块内

解法:

  • synchronized 范围应扩大至代理方法调用
  • 使用 select ... for update 替换 select

版权声明:本文为jhgjyfgyu原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/jhgjyfgyu/article/details/128207115