Spring中事务失效的场景

时间:2022-12-08 07:53:33


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);
      }
  }

Spring中事务失效的场景
可见事务并未回滚

1.2 解决办法

Spring中事务失效的场景

 @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);
      }
  }

Spring中事务失效的场景
Spring中事务失效的场景

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

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();
      }
  }

Spring中事务失效的场景
可见事务还是进行了提交,并未回滚

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);
     }
 }

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

Spring中事务失效的场景

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

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

解法1:异常原样抛出

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

3.1 异常演示

Spring中事务失效的场景

 @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 解决办法

抛出异常或者手动设置事务状态进行回滚
Spring中事务失效的场景

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);
      }
  }

Spring中事务失效的场景
无事务的开启和提交

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);
     }
 }

Spring中事务失效的场景

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);
 }

Spring中事务失效的场景

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");
 }
}

Spring中事务失效的场景
由运行结果结果可知:

事务只开启了一次,并且打印的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");
    }
}

Spring中事务失效的场景

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

另一种方法同理,只不过要在配置时加上@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));

Spring中事务失效的场景
可知数据库的数据已经出现了问题

7.2 解决办法

  • synchronized 范围应扩大至代理方法调用
    Spring中事务失效的场景

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

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

8 @Transactional 方法导致的 synchronized 失效

原因:

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

解法:

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