基础知识:
- Bean单例并不是单例模式,而是单例池以Map形式管理的同单例名的同一对象,默认是该类的首字母小写存入Map(单例池讲解视频1或参考个人的spring笔记或讲解视频2)
- 当多个线程调用同一个单例,会用·ThreadLocal形成线程副本,线程之间是相互隔离的。
- Spring对事务的封装,其实是对Connection对象的封装,connection对象是单例Bean,当多个线程进行访问时,会在每个线程形成ThreadLocal副本,线程间的connection是互不影响的
- 所以事务只能在同一个线程中传播
- this.a()就相当于直接调用a(),this指的是本实例
AOP的自调用、重注入和异步调用:
- AOP是基于JDK的动态代理来实现的,这个可以见个人的spring学习笔记或者学习视频
- 以下是从动态代理角度分析出的结论,可以直接使用,有兴趣可以结合上述资料进行分析
- ① 类在实现AOP代理后,注入使用的就是代理对象,而不是原对象了
- ②本类重新注入和注入别的类的实例,都是引入了新的的实例对象
- ③同一实例的自调用,被调用方法会被抹去aop特性而走原始对象,不同实例间的调用,AOP特性不会受到影响
- ④类中存在重新注入又存在异步调用的时候,启动时会报错,建议把异步调用放在没有重新注入的类中
事务的自调用、重注入以及异步调用:
- Spring事务也是用AOP来实现的,它的后置通知是connection.commit,它的异常通知是connection.rollback
- 既然是用AOP来实现的,那么上述AOP的性质,事务同样拥有,下面是一些结论
- ①事务中的异常如果被捕获了,没有被throw或者正常抛出,那么是不会回滚的,我们称这个异常被消化掉了,见演示①
- ②根据AOP特性,同一实例的自调用,被调用方会失去代理特性走原始对象,所以同一实例事务的自调用,被调用方将失去事务特性而变成普通方法。又事务传播特性:如果调用头没有事务,那么整个调用链connection是不一致的,有异常也不会回滚,即使被调用方有事务也会失去,见演示②;如果调用头有事务,调用链connection对象是一致的,只要存在没有被捕获的异常就会回滚,虽然被调用方事务特性失去了,见演示③
- ③类的重注入和注入其他类的实例,都是引入了新的实例,只不过存在重注入的类又存在异步调用启动会报错,见演示④
- ④根据AOP特性,不同实例间AOP方法的调用,不会影响AOP方法的特性,走的是代理对象,就事务而言,不同实例间的调用,不会影响被调用方的事务特性,又事务的传播特性,调用链中,第一个生效的事务A将会把connection对象传至最后,A及A之后的链路出现未捕获的异常,connection将会回滚,但A之前的链路不会回滚,注意如果要捕获这个异常,必须在该异常节点链路上游中最近的事务生效的节点以及之前进行捕获,才能保证connection对象不会回退,见演示⑤
- ⑤上述讲的都是同步的情况,即在同一个线程中的情况,下面讲异步的情况
- ⑥类中实现异步的前提是不能有重注入,见演示④
- ⑦异步调用的话,事务AOP方面的性质和同步调用一样,即实例的自调用,被调用方将会失去事务特性而变成普通方法,实例间(自注入或注入它类的实例)的调用,被调用方不会失去事务特性,这个可以从动态代理方面分析。但是传播特性这一性质发生了改变,由上可知,connection对象是单例bean,在单例池中以Map形式管理,当多个线程同时访问connection对象时,会用Thread Local在每个线程中形成一个副本,线程之间互不影响,所以每个线程connection对象都是不一样的,connection对象只能在同一个线程中传播,回滚,一个线程中connection对象的回滚,并不会影响到另一个线程connection对象,因为他们是不一样的,见演示⑥
- ⑧另外说一下,@async 异步调用标签也是用AOP做的,所以遵守AOP的特性,即实例自调用就会失效,实例间的调用才会生效,见演示⑦。
- 那么,如果要实现部分数据的提交,部分数据的回退应该怎么做呢?
(1). A方法调用B,属于实例自调用,B方法失去事务特性。
(2).B方法调用A’,属于实例间调用,但是A’并没有事务。
(3).A’方法调用B’,属于实例自调用,B’方法失去事务特性。
(4).B’方法调用E,属于实例间调用,E方法事务生效,且connection开始向后传播。
(5).E方法调用C,属于实例间调用,C方法事务生效,且connection和E方法一致。
(6).C方法调用F,属于实例间调用,F方法没有事务,但是connection和E,C方法一致。
(7).所以A到B’无论哪个环节出现异常都不会回滚,因为事务都没生效,connection也不一致。
(8).E到F无论哪个环节出现异常,且E到F没有捕获(try{}catch(){}),E到F整个链路都会回滚。
(9).E到F如果F环节出现异常,F链路上游中,最近的事务生效的节点是C,所以异常必须在F或者C中捕获,才能保证整个connection对象不会回滚。
(10).E到F如果C环节出现异常,C链路上游中,最近事务生效的节点是C,所以异常必须在C中捕获,才能保证整个connection对象不会回滚。
(11).E到F如果E环节出现异常,E链路上游中,最近事务生效的节点是E,所以异常必须在E中捕获,才能保证整个connection对象不会回滚。
(1). A中注入B,A中有a、b、c、d方法,B中有e、f、g、h方法,他们都有事务和异步上的特性
(2).根据以上说明,@async也是用AOP来做的,所以也遵守AOP特性
(3).下面说以上的调用过程:
(4).a调用h,属于实例间的调用,根据AOP特性,h的事务特性生效,且connection对象开始向后传播
(5).h调用e,属于自调用,e没有事务特性,但是由于h事务的生效,e和h公用同一个connection对象
(6).e调用f,属于自调用,f有@saync注解和事务,根据AOP特性,全部失效,变为普通方法,又事务的传播特性,f、h、e公用同一个connection对象
(7).f调用g,属于自调用,所以f的事务失效,又f使用的是thread来进行异步调用,所以会到另一个线程中执行,h的connection将不会传播至g,事务的传播特性也就此中断
(8).g调用e,属于自调用,又g的事务特性由于自调用原因失去了,且h的传播特性到g由于异步的原因也失去了,所以g和e的connection对象将不再一致
(9).e调用b,属于实例间调用,@async是用AOP做的,事务也是AOP,所以全部生效,b将在另一个线程中开启事务,且将connection对象传递下去。
(10).b调用a,属于自调用,a的事务失效,但是b的事务生效,所以connection将传播至a,a、b connection一致
(11).a调用d,属于自调用,d没有事务,但是线程中b的事务生效使得a、b、d的connection一致。
(12).所以,a、g、e即使出错也不会发生回滚,根据异常节点链路上游中最近的事务生效的节点以及之前进行捕获,才能保证connection对象不会回退,h、e、f中,h、f为事务生效的节点,所以,f出现异常,必须在f中捕获才不会回滚,e出现异常,可以在e和h中捕获,h中出现异常,必须在h中捕获才不会回滚。同理,b、a、d段也是这么处理。
事务的相关演示:
-
演示①:事务只有在throw或者正常发生异常才会回滚,被捕获是不会回滚的
-
演示②:自调用,被调用方的事务特性将会失去
-
演示③:含有事务的调用头调用链路的异常问题
-
演示④:异步调用+重注入出错
-
演示⑤:实例间调用,事务生效部分才会回滚
(1).c调用d,因为是自调用,所以d事务失效
(2).d调用a,是实例间调用,a事务生效,并开始将connection对象向后传
(3).a调用e,是实例间调用,e事务生效,并和左边test1的connection一致
(4).e调用b,实例间调用,虽然b没有事务,但是依然会接收到e的connection对象
(5).由于a事务的生效以及事务的传播,使得a、b和e connection一致,且抛出了异常没有被捕获到,所以connection对象发生了回滚,即a、b以及e发生了回滚,没有存入数据库,但是c和td不会发生回滚,因为不在一个connection对象上。
(6)如果b抛出的异常,根据事务的第6条,必须在该异常节点链路上游中最近的事务生效的节点以及之前进行捕获,所以,必须在b或者e中进行捕获,否则会造成整个connection对象的回退,即a,e ,b方法的回滚。
演示6:异步调用下,事务的传播及回滚问题
上述流程可以简化为
(1).A拥有事务,会将connection向后传递,如果是同步调用的话,B会与A的connection一致,B抛出异常A回滚,但是异步调用会阻断connection的传递,A与B不再共用一个connection对象,B抛异常并不会导致A的回滚
(2)…A拥有事务,会将connection向后传递,如果是同步调用的话,C也会变成事务生效的节点且会与A的connection一致,即ACD共用一个connection,但是异步调用会阻断connection对象的传递,A用一个connection对象,CD共用一个connection对象,D抛异常会导致C的回滚而不会导致A的回滚
(3).所以A提交,C回滚
演示7:@async同步调用失效,异步调用生效
可见test1和test2用了同一个线程,异步注解并没有生效,test1和test3用的不是同一个线程,异步注解生效,test1和test4,不用AOP的方式,以最原始的方式也可以实现异步调用。