zoukankan      html  css  js  c++  java
  • 嵌套事务总结

    最近线上发生了一起故障,是关于嵌套事务未回滚的问题,这里记录一下。

    发生故障的场景是:

     主方法parent()里调child()方法,当child()抛出异常时,parent()和child()均未回滚。背景先介绍到这里,你可以先想想为什么没回滚,下面由浅入深讲解。

    ------------------------------------华丽的分割线---------------------------------------------------

    一、场景分析
     场景A:


     

     这里是分别执行了两个事务,执行的结果是两个方法都可以插入数据!

     

    场景B:

    修改上述代码如下:

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

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

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

    场景C:

    修改child()的代码如下所示,其他代码和场景B一样:

    会出现异常,并且数据全都没有插入进去:

    疑问1:场景C中child()抛出了异常,但是parent()没有抛出异常,按道理是不是应该parent()提交成功而child()回滚?
    可能有的小伙伴要说了,child()抛出了异常在parent()没有进行捕获,造成了parent()也是抛出了异常了的!所以他们两个都会回滚?

    场景D:

    按照上述小伙伴的疑问这个时候,如果对parent()方法修改,捕获child()中抛出的异常,其他代码和场景C一样:

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

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

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

    二、问题本质所在

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

    场景C的本质:

     场景D的本质:

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

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

    首先我们看一下一个简单的动态代理实现方式:

     

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

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

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

     执行结果如下:

    这里可以很清楚的看出来say()走的是代理,而say2()走的是普通的方法,没有经过代理!看到这里你是否已经恍然大明白了呢?

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

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

    记住:只有代理对象proxy直接调用的那个方法才是真正的走代理的!

    四 、如何解决这个问题?

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

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

    1         <dependency>
    2             <groupId>org.springframework.boot</groupId>
    3             <artifactId>spring-boot-starter-aop</artifactId>
    4             <version>2.0.3.RELEASE</version>
    5         </dependency>

    添加注解

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

     可见,child方法由于异常已经回滚了,而parent可以正确的提交,这才是我们想要的结果!注意的是在parent调用child的时候是通过try/catch捕获了异常的!

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

     这个时候parent()和child()两个方法在同一个事务里,child()抛异常的话,两个方法都会回滚的。

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

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

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

    <aop:aspectj-autoproxy expose-proxy="true"/>

    五、总结

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

     主方法parent()里调child()方法,当child()抛出异常时,parent()和child()均未回滚。这是因为parent调child方法,就是在调一个普通方法,即使child()上写了@Transactional,其本质就是:

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

    ————————————————
    版权声明:本文为CSDN博主「Java枫」的原创文章。
    原文链接:https://blog.csdn.net/qq_33101675/article/details/83012379

  • 相关阅读:
    POJ 3660 Cow Contest (floyd求联通关系)
    POJ 3660 Cow Contest (最短路dijkstra)
    POJ 1860 Currency Exchange (bellman-ford判负环)
    POJ 3268 Silver Cow Party (最短路dijkstra)
    POJ 1679 The Unique MST (最小生成树)
    POJ 3026 Borg Maze (最小生成树)
    HDU 4891 The Great Pan (模拟)
    HDU 4950 Monster (水题)
    URAL 2040 Palindromes and Super Abilities 2 (回文自动机)
    URAL 2037 Richness of binary words (回文子串,找规律)
  • 原文地址:https://www.cnblogs.com/hmy-1365/p/13064111.html
Copyright © 2011-2022 走看看