spring事务:同一个类中无事务方法a()内部调用有事务方法b()的问题

同一类中a()方法没有@Transactional 注解,在其内部调用有@Transactional 注解的方法,有@Transactional 注解的方法b()的事务被忽略,不会发生回滚。

1. 事务的4种特性  

序号       参数                                    含义
1    原子性(Atomicity)            事务是数据库的逻辑工作单位,它对数据库的修改要么全部执行,要么全部不执行。
2    一致性(Consistemcy)      事务前后,数据库的状态都满足所有的完整性约束。
3    隔离性(Isolation)             并发执行的事务是隔离的,一个不影响一个。通过设置数据库的隔离级别,可以达到不同的隔离效果
4    持久性(Durability)            在事务完成以后,该事务所对数据库所作的更改便持久的保存在数据库之中,并不会被回滚。
 

2.Transactional()控制事务传播的配置项目(默认Propagation.REQUIRED)

   @Transactional(propagation=Propagation.REQUIRED)           //控制事务传播。默认是Propagation.REQUIRED
    @Transactional(isolation=Isolation.DEFAULT)                //控制事务隔离级别。默认跟数据库的隔离级别相同
    @Transactional(readOnly=false)                             //控制事务可读写、只可读。默认可读写
    @Transactional(timeout=30)                                 //控制事务的超时时间,单位秒。默认跟数据库的事务控制系统相同
    @Transactional(rollbackFor=RuntimeException.class)         //控制事务遇到哪些异常会回滚。默认是RuntimeException
    @Transactional(rollbackForClassName=RuntimeException)      //同上
    @Transactional(noRollbackFor=NullPointerException.class)   //控制事务遇到哪些异常不会回滚。默认遇到RuntimeException回滚
    @Transactional(noRollbackForClassName=NullPointerException)//同上

3.事务的7中传播特性4. 事务的传播案例:

spring事务:同一个类中无事务方法a()内部调用有事务方法b()的问题_第1张图片

spring事务:同一个类中无事务方法a()内部调用有事务方法b()的问题_第2张图片

 注:这里的Propagation是事务的传播行为,默认是REQUIRED,意思是如果当前没有事务,就开启一个事务,如果已经存在一个事务,就加入到这个事务中;REQUIRES_NEW是说,新建事务,如果当前存在事务,把当前事务挂起;意思是这里执行到child()方法时,parent所在的事务就会挂起,方法child就会起一个新的事务,等待方法child的事务完成以后,方法parent才继续执行。

执行的结果是两个方法都可以插入数据!

场景A场景B都是正常的执行,期间没有发生任何的回滚,假如child()方法中出现了异常! 

spring事务:同一个类中无事务方法a()内部调用有事务方法b()的问题_第3张图片会出现异常,并且数据全都没有插入进去:

疑问1:场景C中child()抛出了异常,但是parent()没有抛出异常,按道理是不是应该parent()提交成功而child()回滚?
可能有的小伙伴要说了,child()抛出了异常在parent()没有进行捕获,造成了parent()也是抛出了异常了的!所以他们两个都会回滚?
spring事务:同一个类中无事务方法a()内部调用有事务方法b()的问题_第4张图片

然后再次执行,结果是两个都插入了数据库:

看到这里很多小伙伴都可能会问,按照我们的逻辑来想的话child()中抛出了异常,parent()没有抛出并且捕获了child()抛出了异常!执行的结果应该是child()回滚,parent()提交成功的啊!

疑问2:为什么不是child()回滚和parent()提交成功哪?

问题本质所在

我们知道Spring事务管理是通过JDK动态代理的方式进行实现的(另一种是使用CGLib动态代理实现的),也正是因为动态代理的特性造成了上述parent()方法调用child()方法的时候造成了child()方法中的事务失效!简单的来说,在场景D中parent()方法调用child()方法的时候,child()方法的事务是不起作用的,此时的child()方法像一个没有加事务的普通方法,其本质上就相当于下边的代码:

spring事务:同一个类中无事务方法a()内部调用有事务方法b()的问题_第5张图片

正如上述的代码,我们可以很轻松的解释疑问1和疑问2,因为动态代理的特性造成了场景C和场景D的本质如上述代码。在场景C中,child()抛出异常没有捕获,相当于parent事务中抛出了异常,造成parent()一起回滚,因为他们本质是同一个方法;在场景D中,child()抛出异常并进行了捕获,parent事务中没有抛出异常,parent()和child()同时在一个事务里边,所以他们都成功了;
看到这里,那么动态代理的这个特性到底是什么才会造成Spring事务失效呐?

动态代理的这个特性到底是什么?

spring事务:同一个类中无事务方法a()内部调用有事务方法b()的问题_第6张图片

spring事务:同一个类中无事务方法a()内部调用有事务方法b()的问题_第7张图片

spring事务:同一个类中无事务方法a()内部调用有事务方法b()的问题_第8张图片

 此时我们执行以下测试方法,注意了此时是同时调用了say()say2()的,执行结果如下:

spring事务:同一个类中无事务方法a()内部调用有事务方法b()的问题_第9张图片

可以看出,在HelloImpl 类中由于say()没有调用say2(),他们方法的执行都是使用了代理的,也就是说say和say2都是通过代理对象调用的invoke()方法,这和我们场景A类似。

假如我们模拟一下场景C和场景D在say()中调用say2(),那么代码修改为如下:

spring事务:同一个类中无事务方法a()内部调用有事务方法b()的问题_第10张图片

执行结果如下:

spring事务:同一个类中无事务方法a()内部调用有事务方法b()的问题_第11张图片 这里可以很清楚的看出来say()走的是代理,而say2()走的是普通的方法,没有经过代理!看到这里你是否已经恍然大明白了呢?

这个应该可以很好的理解为什么是这样子!这是因为在Java中say()中调用say2()中的方法,本质上就相当于把say2()的方法体放入到say()中,也就是内部方法,同样的不管你嵌套了多少层,只有代理对象proxy直接调用的那一个方法才是真正的走代理的,如下:
spring事务:同一个类中无事务方法a()内部调用有事务方法b()的问题_第12张图片

 测试方法和上边的测试方法一样,执行结果如下:

 spring事务:同一个类中无事务方法a()内部调用有事务方法b()的问题_第13张图片

 如何解决这个问题?

上文的分析中我们已经了解了为什么在该特定场景下使用Spring事务的时候造成事务无法回滚的问题,下边我们谈一下几种解决的方法:

1、我们可以选择逃避这个问题!我们可以不使用以上这种事务嵌套的方式来解决问题,最简单的方法就是把问题提到Service或者是更靠前的逻辑中去解决,使用service.xxxtransaction是不会出现这种问题的。
2、通过AopProxy上下文获取代理对象:
(1)SpringBoot配置方式:注解开启 exposeProxy = true,暴露代理对象 (否则AopContext.currentProxy()) 会抛出异常。
添加依赖

        
            org.springframework.boot
            spring-boot-starter-aop
            2.0.3.RELEASE
        

添加注解

spring事务:同一个类中无事务方法a()内部调用有事务方法b()的问题_第14张图片

修改原有代码的执行方式为

spring事务:同一个类中无事务方法a()内部调用有事务方法b()的问题_第15张图片可见,child方法由于异常已经回滚了,而parent可以正确的提交,这才是我们想要的结果!注意的是在parent调用child的时候是通过try/catch捕获了异常的!

如果我们把child()事务传播类型改为REQUIRED的话

spring事务:同一个类中无事务方法a()内部调用有事务方法b()的问题_第16张图片这个时候parent()和child()两个方法在同一个事务里,child()抛异常的话,两个方法都会回滚的。

如果在parent方法内把try..catch..去掉的话

spring事务:同一个类中无事务方法a()内部调用有事务方法b()的问题_第17张图片

两个方法都会回滚的,因为child()方法是起了一个新的事务,他会回滚,然后异常往上抛,parent()也会回滚。

(2)传统Spring XML配置文件只需要添加依赖个设置如下配置即可,使用方式一样:

4.举个上手的按例:事务在A类的a()方法中调用B类的b()方法的传播案例

spring事务:同一个类中无事务方法a()内部调用有事务方法b()的问题_第18张图片

总结

这里回到文章首页线上故障场景拿过来

spring事务:同一个类中无事务方法a()内部调用有事务方法b()的问题_第19张图片主方法parent()里调child()方法,当child()抛出异常时,parent()和child()均未回滚。这是因为parent调child方法,就是在调一个普通方法,即使child()上写了@Transactional,其本质就是:

spring事务:同一个类中无事务方法a()内部调用有事务方法b()的问题_第20张图片

 内层方法抛异常,但是被catch到,自始至终都没有触发异常来回滚。

你可能感兴趣的:(事务,spring)