Spring事务的的详细理解,事务嵌套解析,以及事务失效的场景解惑
想要了解Spring的事务嵌套,我们先了解一下Spring的七种事务传播属性各自表示的意思
-
propagation_requierd
:如果当前没有事务,就新建一个事务,如果已存在一个事务中,加入到这个事务中,这是最常见的选择。 -
propagation_supports
:支持当前事务,如果没有当前事务,就以非事务方法执行。 -
propagation_mandatory
:使用当前事务,如果没有当前事务,就抛出异常。 -
propagation_required_new
:新建事务,如果当前存在事务,把当前事务挂起。 -
propagation_not_supported
:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 -
propagation_never
:以非事务方式执行操作,如果当前事务存在则抛出异常。 -
propagation_nested
:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与propagation_required类似的操作
了解了这七种传播属性表示的意思,接下来在说具体的解释
详解Spring的事务传播属性以及在写代码的过程中发生嵌套并发生事务失效的场景
再说这些之前,大家先要清除一个问题,Spring的事务是怎么实现的?
“Spring本身是没有事务的,只有数据库才会回有事务,而Spring的事务是借助AOP,通过动态代理的方式,在我们要操作数据库的是时候,实际是Spring通过动态代理进行功能扩展,在我们的代码操作数据库之前通过数据库客户端打开数据库事务,如果代码执行完毕没有异常信息或者是没有Spring要捕获的异常信息时,再通过数据库客户端程序提交事务,如果有异常信息或者是有Spring要捕获的异常信息,再通过数据库客户端程序回滚事务,从而达到控制数据库事务的目的。
1、Spring的事务的简单原理示意图,请看下图
2、Spring的Transactional注解默认情况下的传播属性是什么呢,请看一下Transactional的源码:下图所示(所以在默认的情况下我们加了Transactional 就会有事务)
3、第1、第2两部分看明白之后,再来说一下Spring事务嵌套会发生的情况
3.1、在同一个类中发生事务嵌套的情况
答案是会回滚的,原因是什么呢?接着看下边的示意图
看上边的示意图你一定会明白了吧,原因还是因为代理的时候,直接把没有事务的方法包在了有事务的代理方法里边了自然就有事务了,这样说够明确吗?不够的话可以留言,给你解答一下
3.2 再看一种嵌套的方式
答案:这时候调用test02,test01有异常发生时是不会回滚的
为什么呢?其实如果上边的你理解了下边就很容易理解了
3.3 这些明白了 后边的就更容易明白了,再看一个例子
上述3.3中的例子,如果调用test02 的话,当程序发生异常,test01会不会回滚呢?
答案是会回滚的 原因和3.1中的情况类似
所以综合上边说的,在同一个类中事务嵌套的话,最终的结果应该是取决于最外层的方法事务的传播特性
4、以上是在一个相同的类中的情况,如果是不同的类中的方法嵌套调用是什么样子的呢?
4.1 我们来看下列一种情况
在TestService3中调用TestService4中的没有事务的方法,会发生什么呢?TestService4中的test41方法发生异常会回滚吗?
答案:会的
为什么呢请看下图:
4.2 我们再来看一个情况
TestService3中的方法是有事务的,TestService4中的方法是以非事务的方式运行,如果存在事务就挂起事务,那么自然就没有事物了
所以上述的情况就是,如果TestService4中的方法报出异常,则TestService3中的数据可以回滚,但是TestService4中操作的数据是不会回滚的
再有其他的情况大家根据上述的原理,然后结合传播属性的特点去套就可以了
总结4中的案例得到的结论是,如果是不同的类的事务嵌套的话,外层的方法按照外层的事务传播属性执行,内层的传播属性按照内层的传播属性的特点去运行
5、下边介绍一些目前我知道的Spring事务会失效的情况
5.1 事务方法访问修饰符不是public,导致事务失效
比如下图中的代码
其实如果是JDK的动态代理 就不允许这种情况,因为JDK动态代理需要有接口,而接口中是不能有 私有方法的,如果是CGLIB动态代理的话也是不允许的代理private方法的
5.2 如果事务方法是static、final的,同样无法通过动态代理,事务也是不会生效的。
Spring的声明式事务是基于动态代理实现的,我们无法重写final修饰的方法;不管是JDK动态代理还是Cglib的动态代理,就是要通过代理的方式获取到代理的具体对象,而static方法修饰的方法是属于类对象的,不属于任何实例对象,所以static方法不能被重写也就是说static方法也不被进行动态代理。
5.3 操作的数据库表如果本身不支持事务 那么配置的Spring事务 也会失效
5.4 代码中的异常被 catch 住,而且没有异常再次抛出,也会让spring的事务失效,如下图中所示 事务就会失效
想要 避免上述的失效的话 可以 在catch 住后在catch块中再次 抛出Spring事务支持的异常,就可以了
5.5 多线程的调用导致事务的失效,例如 下边的案例 会导致 test31的事务失效
解释:原因是因为 test31中操作数据要回滚,需要有异常抛出才可以回滚,但是由于线程异步运行这种写法,test31和test41分别是两个独立的事务,他们的数据库链接都可能是不同的,要想回滚则test31和test41都应该有异常抛出才可以。
但是test31所在的线程没有捕获到test41所在的子线程的异常所以没有回滚,之所以没有捕获到test41所在的线程的异常是因为test41相对test31是异步运行,可能test31已经运行完了,但是test41还没有运行完,所以在写这种代码的时候大家可以使用thread.setUncaughtExceptionHandler
方法来处理线程内部的异常,也就是当thread报出异常的时候可以在这个方法中再次抛出异常这样可以触发外部事务的回滚,或者也可以采用下列的代码
@Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
public void test31() throws ExecutionException, InterruptedException {
Map<String,Object> map=new HashMap<>();
map.put("id",3);
map.put("quantity",300);
int i = testDao.updateStock(map);
FutureTask task=new FutureTask(new Callable() {
@Override
public Object call() throws Exception {
testService4.test41();
return "ok";
}
});
Thread thread = new Thread(task);
thread.start();
//等待执行结果,当有异常的时候在这里就可以获取到异常传递给test31触发他的回滚
Object ok = task.get();
System.out.println("=============OK");
}