SpringBoot异常处理回滚事务详解(自动回滚、手动回滚、部分回滚)(事务失效)


参考:https://blog.csdn.net/zzhongcy/article/details/102893309

集合。事务是现代数据库理论中的核心概念之一。如果一组处理步骤或者全部发生或者一步也不执行,我们称该组处理步骤为一个事务。当所有的步骤像一个操作一样被完整地执行,我们称该事务被提交。由于其中的一部分或多步执行失败,导致没有步骤被提交,则事务必须回滚到最初的系统状态。


原子性:一个事务中所有对数据库的操作是一个不可分割的操作序列,要么全做要么全不做

2.一致性:数据不会因为事务的执行而遭到破坏

3.隔离性:一个事物的执行,不受其他事务的干扰,即并发执行的事物之间互不干扰

4.持久性:一个事物一旦提交,它对数据库的改变就是永久的。


Transactional 的目标方法时,Spring Framework 默认使用 AOP 代理,在代码运行时生成一个代理对象,根据 @Transactional 的属性配置信息,这个代理对象决定该声明 @Transactional 的目标方法是否由拦截器 TransactionInterceptor 来使用拦截,在 TransactionInterceptor 拦截时,会在目标方法开始执行之前创建并加入事务,并执行目标方法的逻辑,最后根据执行情况是否出现异常,利用抽象事务管理器 AbstractPlatformTransactionManager 操作数据源 DataSource 提交或回滚事务。

Spring AOP 代理有 CglibAopProxy 和 JdkDynamicAopProxy 两种,以 CglibAopProxy 为例,对于 CglibAopProxy,需要调用其内部类的 DynamicAdvisedInterceptor 的 intercept 方法。对于 JdkDynamicAopProxy,需要调用其 invoke 方法。


事务回滚,也就是默认对 RuntimeException() 异常或是其子类进行事务回滚。

如果是checked异常则不回滚,例如空指针异常、算数异常等会被回滚;文件读写、网络问题Spring就没法回滚。

若想对所有异常(包括自定义异常)都起作用,注解上面需配置异常类型:@Transactional(rollbackFor = Exception.class

  • 数据库要支持事务,如果是mysql,要使用innodb引擎,myisam不支持事务

  • 事务@Transactional由spring控制时,它会在抛出异常的时候进行回滚。如果自己使用try-catch捕获处理了,是不生效的。如果想事务生效可以进行手动回滚或者在catch里面将异常抛出【throw new RuntimeException();】

    • 方案一:手动抛出运行时异常(缺陷是不能在catch代码块自定义返回值)

        try{
            ....  
        }catch(Exception e){
            logger.error("",e);
            throw new RuntimeException;
        }
    • 方案二:手动进行回滚【 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); 】

        try{
            ...
        }catch(Exception e){
            log.error("fail",e);
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            return false;
        }
  • @Transactional可以放在Controller下面直接起作用,看到网上好多同学说要放到@Component下面或者@Service下面,经过试验,可以不用放在这两个下面也起作用。

  • @Transactional引入包问题,它有两个包:

    import javax.transaction.Transactional; 
    // 和
    import org.springframework.transaction.annotation.Transactional; 		// 推荐

    这两个都可以用,对比了一下他们两个的方法和属性,发现后面的比前面的强大。建议使用后面的。


  • rollback 回滚事务。它的实现类是 AbstractPlatformTransactionManager。

    @Autowired
    priDataSourceTransactionManager dataSourceTransactionManager;
    @Autowired
    TransactionDefinition transactionDefinition;
    
    // 手动创建事务
    TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);
    
    // 手动提交事务
    dataSourceTransactionManager.commit(transactionStatus);
    
    // 手动回滚事务。(最好是放在catch 里面,防止程序异常而事务一直卡在哪里未提交)
    dataSourceTransactionManager.rollback(transactionStatus);

    事务失效不回滚的问题

    异常被捕获导致事务失效

    在spring boot 中,使用事务非常简单,直接在方法上面加入@Transactional 就可以实现,以下是我的做法

    @GetMapping("delete")
    @ResponseBody
    @Transactional    
    public void delete(@RequestParam("id") int id) {       
    	try {          
            //delete country
            this.repository.delete(id);         
            if(id == 1){              
                throw Exception("测试事务");
            }           
             //delete city
            this.repository.deleteByCountryId(id);
        }catch (Exception e){
            logger.error("delete false:" + e.getMessage());        
            return new MessageBean(101,"delete false");
        }
    }

    发现事务不回滚,即 this.repository.delete(id);成功把数据删除了。

    原因:

    默认spring事务只在发生未被捕获的 RuntimeException 时才回滚。

    spring aop 异常捕获原理:被拦截的方法需显式抛出异常,并不能经任何处理,这样aop代理才能捕获到方法的异常,才能进行回滚,默认情况下aop只捕获 RuntimeException 的异常,但可以通过配置来捕获特定的异常并回滚。

    换句话说在service的方法中不使用 try catch 或者在 catch 中最后加上throw new RuntimeExcetpion()抛出运行异常,这样程序异常时才能被aop捕获进而回滚。

    解决方案:

    • 方案1:例如service层处理事务,那么service中的方法中不做异常捕获,或者在catch语句中最后增加

      throw new RuntimeException(); 语句,以便让aop捕获异常再去回滚,并且在service的上层要继续捕获这个异常。

    • 方案2:在service层方法的catch语句中进行手动回滚,这样上层就无需去处理异常。

      @GetMapping("delete") 
      @ResponseBody 
      @Transactional 
      public Object delete(@RequestParam("id") int id){ 
          if (id < 1){
               return new MessageBean(101,"parameter wrong: id = " + id) ; 
           } 
          try { 
               //delete country
               this.countryRepository.delete(id);
               //delete city
               this.cityRepository.deleteByCountryId(id);
               return new MessageBean(200,"delete success");
           }catch (Exception e){
               logger.error("delete false:" + e.getMessage());
               // 手动回滚
               TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
               return new MessageBean(101,"delete false");
           }
      }

    自调用导致事务失效

    在 Spring 的 AOP 代理下,只有目标方法由外部调用,目标方法才由 Spring 生成的代理对象来管理,否则会造成自调用问题。

    若同一类中的 没有@Transactional 注解的方法 内部调用 有@Transactional 注解的方法,有@Transactional 注解的方法的事务被忽略,不会发生回滚。见 示例代码展示。

    自调用问题示例:

    @Service
    public class OrderService {
        
        private void insert() {
            insertOrder();
        }
    
        @Transactional
        public void insertOrder() {
            //insert log info
            //insertOrder
            //updateAccount
        }
    }
    
    // insertOrder() 尽管有@Transactional 注解,但它被内部方法 insert()调用,事务被忽略,出现异常事务不会发生回滚,并且会报错类似于:
    // org.springframework.transaction.NoTransactionException: No transaction aspect-managed TransactionStatus in scope(翻译:没有Transaction无法回滚事务。自调用导致@Transactional 失效。)

    自调用失效原因:

    • spring里事务是用注解配置的,当一个方法没有接口,单单只是一个内部方法时,事务的注解是不起作用的,需要回滚时就会报错。

    • 出现这个问题的根本原因在于AOP的实现原理。由于@Transactional 的实现原理是AOP,AOP的实现原理是动态代理,换句话说,自调用时不存在代理对象的调用,这时不会产生我们注解@Transactional 配置的参数,自然无效了。

    • 虽然可以直接从容器中获取代理对象,但这样有侵入之嫌,不推荐。

    解决方案:

    1、可以在类服务上加事务(或者最外层的接口方法上加事务):

    @Transactional
    public class MyTransactional {
        
        //@Transactional  服务上加了@Transactional后  这里可以不加
        public UserEntity login(UserEntity user) {
            this.test();
            return user;
        }
    
        public void test(){
            userDao.update(6);
            if(userDao.update(6)){
                TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        	}
    	}
    }

    2、使用AspectJ 取代 Spring AOP 代理

    上面的两个问题@Transactional 注解只应用到 public 方法和自调用问题,是由于使用 Spring AOP 代理造成的。为解决这两个问题,可以使用 AspectJ 取代 Spring AOP 代理。

    需要将下面的 AspectJ 信息添加到 xml 配置信息中。

    AspectJ 的 xml 配置信息

    <tx:annotation-driven mode="aspectj" />
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    bean>
    bean class="org.springframework.transaction.aspectj.AnnotationTransactionAspect" factory-method="aspectOf">
    	<property name="transactionManager" ref="transactionManager" />
    bean>

    同时在 Maven 的 pom 文件中加入 spring-aspects 和 aspectjrt 的 dependency 以及 aspectj-maven-plugin。

    AspectJ 的 pom 配置信息

    <dependency>
        <groupId>org.springframeworkgroupId>
        <artifactId>spring-aspectsartifactId>
        <version>4.3.2.RELEASEversion>
        dependency>
    <dependency>
        <groupId>org.aspectjgroupId>
        <artifactId>aspectjrtartifactId>
        <version>1.8.9version>
    dependency>
    
    <plugin>
    <groupId>org.codehaus.mojogroupId>
    <artifactId>aspectj-maven-pluginartifactId>
    <version>1.9version>
    <configuration>
        <showWeaveInfo>trueshowWeaveInfo>
        <aspectLibraries>
        <aspectLibrary>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-aspectsartifactId>
            aspectLibrary>
        aspectLibraries>
    configuration>
    
    <executions>
    <execution>
        <goals>
            <goal>compilegoal>
            <goal>test-compilegoal>
        goals>
    execution>
    executions>
    plugin>

     

    其他

    事务提交方式

    默认情况下,数据库处于自动提交模式。每一条语句处于一个单独的事务中,在这条语句执行完毕时,如果执行成功则隐式的提交事务,如果执行失败则隐式的回滚事务。

    对于正常的事务管理,是一组相关的操作处于一个事务之中,因此必须关闭数据库的自动提交模式。不过,这个我们不用担心,spring 会将底层连接的【自动提交特性】设置为 false 。也就是在使用 spring 进行事物管理的时候,spring 会将【是否自动提交】设置为false,等价于JDBC中的 connection.setAutoCommit(false); ,在执行完之后在进行提交 connection.commit(); 。


    事务回滚规则

    指示spring事务管理器回滚一个事务的推荐方法是在当前事务的上下文内抛出异常。spring事务管理器会捕捉任何未处理的异常,然后依据规则决定是否回滚抛出异常的事务。

    默认配置下,spring只有在抛出的异常为运行时unchecked异常时才回滚该事务,也就是抛出的异常为RuntimeException的子类(Errors也会导致事务回滚),而抛出checked异常则不会导致事务回滚。

    可以明确的配置在抛出那些异常时回滚事务,包括checked异常。也可以明确定义那些异常抛出时不回滚事务。


    事务并发会产生的问题

    术语含义
    脏读 A事务读取到了B事务还未提交的数据,如果B未提交的事务回滚了,那么A事务读取的数据就是无效的,这就是数据脏读
    不可重复读 在同一个事务中,多次读取同一数据返回的结果不一致,这是由于读取事务在进行操作的过程中,如果出现更新事务,它必须等待更新事务执行成功提交完成后才能继续读取数据,这就导致读取事务在前后读取的数据不一致的状况出现
    幻读 A事务读取了几行记录后,B事务插入了新数据,并且提交了插入操作,在后续操作中A事务就会多出几行原本不存在的数据,就像A事务出现幻觉,这就是幻读

    第一类丢失更新

    没有事务隔离的情况下,两个事务都同时更新一行数据,但是第二个事务却中途失败退出, 导致对数据的两个修改都失效了。

    例如:

    张三的工资为5000,事务A中获取工资为5000,事务B获取工资为5000,汇入100,并提交数据库,工资变为5100;
    随后,事务A发生异常,回滚了,恢复张三的工资为5000,这样就导致事务B的更新丢失了。


    脏读

    脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。

    例如:

    张三的工资为5000,事务A中把他的工资改为8000,但事务A尚未提交。
    与此同时,事务B正在读取张三的工资,读取到张三的工资为8000。
    随后,事务A发生异常,回滚了事务,张三的工资又回滚为5000。
    最后,事务B读取到的张三工资为8000的数据即为脏数据,事务B做了一次脏读。


    不可重复读

    是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。

    例如:

    在事务A中,读取到张三的工资为5000,操作没有完成,事务还没提交。
      与此同时,事务B把张三的工资改为8000,并提交了事务。
      随后,在事务A中,再次读取张三的工资,此时工资变为8000。在一个事务中前后两次读取的结果并不致,导致了不可重复读。


    第二类丢失更新

    不可重复读的特例。

    有两个并发事务同时读取同一行数据,然后其中一个对它进行修改提交,而另一个也进行了修改提交。这就会造成第一次写操作失效。

    例如:

    在事务A中,读取到张三的存款为5000,操作没有完成,事务还没提交。
      与此同时,事务B存入1000,把张三的存款改为6000,并提交了事务。
      随后,在事务A中,存储500,把张三的存款改为5500,并提交了事务,这样事务A的更新覆盖了事务B的更新。


    幻读

    是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。

    例如:

    目前工资为5000的员工有10人,事务A读取到所有的工资为5000的人数为10人。
      此时,事务B插入一条工资也为5000的记录。
      这时,事务A再次读取工资为5000的员工,记录为11人。此时产生了幻读。


    不可重复读和幻读的区别

    不可重复读的重点是修改,同样的条件,你读取过的数据,再次读取出来发现值不一样了

    幻读的重点在于新增或者删除,同样的条件,第 1 次和第 2 次读出来的记录数不一样

      来源:https://blog.csdn.net/footless_bird/article/details/119530607