事务没回滚?来,我们从现象到原理一起分析一波!
@Transactional-曾经年少只懂用
当我还是一个初出校园的小程序猿的时候,碰到过这样的一个问题:
需求是有两个表,分别是订单表和订单附加信息表,其中订单表一定要落数据,订单附加信息表保存的时候如果有异常,则回滚,但是不能影响订单表的数据。
美丽的测试小姐姐测试时发现订单表没有数据,于是对我说:大兄弟,瞅一眼,啥情况啊。
我检查代码的时候发现保存订单表和订单附加信息表的方法上都加了@Transactional注解,当保存订单的方法顺利执行完成,如果保存订单附加信息的方法抛出异常也会被捕获。最后得出结论:没毛病呀,很正常呀。
这个时候我完美的体现作为一个程序猿应该的气质,质疑测试的使用环境,方式,甚至执行姿势,站起来对着小姐姐喊到:"再来一笔试一试,我盯着日志!"但是,看到结果的那一刻,我小脸一红,默默的坐了下来,安心的排查起问题来。
场景复现,代码展示:
1.可以Http接口调用,也可以是Dubbo调用。提供一个入口即可。这里为了方便演示,用Http接口调用。代码如下:
2.前面的Controller中注入的就是下面的OrderBizService。这个OrderBizService中注入了保存订单表的IOrderInfoService和保存订单附加信息表的IOrderInfoExtService。代码如下:
3.OrderBizService中有三个关键的点已经在图片中进行了说明。
4.项目整体结构如下:
5.订单表和订单附加信息表结构如下:
通过以上操作,我们进行了场景铺垫、代码复现、表结构展示。我们先列出我们预期的结果:
order_info表中有一条数据,order_info_ext表中没有数据。
那么接下来,我们执行一下看看结果吧。在本地启动成功后,调用链接:http://127.0.0.1:8080/transaction
6.看到执行结果的时候,气氛突然变的尴尬了起来:OMG,这和我的预期不一样啊!到底为啥呢?我用了@Transactional注解了呀。怪只怪自己
曾经年少只懂用,头发全黑且茂密。
看到这里了,大家可以先回想一下自己在项目中写的有关于事务的代码,是不是也是这样操作的:
1.通过Controller调用某个Service的方法,假如是saveMian(这个方法有没有@Transactional注解都可以)
2.Service的saveMian方法里面直接调用了这个Service里面某个有@Transactional注解的方法,假如是saveSub(直接调用:saveSub或者this.saveSub)
3.如果你的回答是有这样的代码的话,那么大兄弟或者小姐姐,我告诉你:如果这个代码别人写的,你踩坑了;如果这个代码是你写的,你留坑了。
如果你去问前辈,那么这个时候前辈就会告诉你:
听哥一句劝:在bean中不要直接调用或者使用this调用,某个被@Transactional注解标注的方法。this下@Transactional注解是不生效的。
@Transactional-现在长大知原理
前面我们已经看到了现象,接下来我们需要深入到原理的部分,一起透过现象看本质了。
可以看出,这个this并不是动态代理的子类对象,而是一个原始对象,所以this调用的orderExt()方法虽然加了@Transactional注解,但是无法却通过动态代理来增强,从而导致事务失效。
那么上面的代码其实就可以简化成下面的样子,这样就一目了然了:
相信通过上面的代码,大家就知道为啥两个表都能插入成功了。想不到吧,这令人窒息的操作!
那么问题来了:是什么原因导致执行结果和我们的预期结果不一致的呢?这个问题我先按下不表。先说说结论,大家带着结论往下看会容易理解一点。
结论就是:Spring的事务是基于AOP实现的,AOP是基于动态代理实现的.所以@Transactional注解如果想要生效,那么其调用方,需要是被Spring动态代理后的类。
在这插一句:当年在大学里面学到AOP和IOC的时候,感觉这两个概念难以理解的同时又很牛逼。但是当我在工作中实践,慢慢理解的时候,我才发现:面向切面编程(AOP)、控制反转(IOC)这两个翻译是多么的精准的描述啊!
现在有了结论,我们知道了关键是调用方是需要被动态代理的类。
那么解决的方法就呼之欲出了,核心点就是如何获得OrderBizService的动态代理对象。那么这里我给出三种解决方案
方案一.自己注入自己
我们把代码改成下面的样子,并打上断点执行起来:
通过调试可以看到,orderExt()方法的调用方,orderBizService已经是被CGLIB动态代理增强后的类了。所以其事务能生效。
方案二.注入ApplicationContext
也是只需要两处改动:
注入ApplicationContext,spring上下文。
从ApplicationContext中获取到OrderBizService。
方案三.借助AopContext
1.首先第一步,需要进行代理暴露。expose-proxy="true"
Aop的代理暴露默认是false。如果我们需要取出当前的代理,那么需要是设置为true。这样,代理类就会被设置到AopContxt中。
在这里,我可以说一下之前说这个aop.xml我觉得很重要的原因。
在spring时代,我们需要对xml进行各种配置。其中一个就是<aop:aspectj-autoproxy/> ,对我而言,我当时也是仅仅知道要加这行代码,不知道为什么要加。随着知识框架的逐渐完善,再回头研究它,发现很快就能理解。假设当初我刚学这一块知识,知识还没成体系,我硬要深入到源码,最后得到的结果也一定是在里面花费了大量时间,得到一个模棱两可的感觉,打击了自己的自信心。
我想表达的意思就是:对于初学者而言,有的时候不必花太多的时候去追求细枝末节,时间会帮你搭建知识体系,有了体系,你回过头看,发现有的问题,迎刃而解。所以,如果作为初学者,你只管保持大方向的学习,保持努力,保持进步,剩下细枝末节的东西,时间会帮你解决。
第2步:引入aop.xml文件:
其实这里用@EnableAspectJAutoProxy(exposeProxy =true)注解也可以,就可以不需要aop.xml文件,而且更加符合springboot的思想。
第3步:从AopContext中取出OrderBizService.
对于AopContext多说两句:
第一句:AopContext里面维护了一个ThreadLocal:
private static final ThreadLocal currentProxy =new NamedThreadLocal<>("Current AOP proxy");
第二句:因为AopContext默认是不暴露当前代理类的,所以要@EnableAspectJAutoProxy(exposeProxy =true)或者<aop:aspectj-autoproxy expose-proxy="true"/>:
三种解决方案分析完了,接下来,我想我们一起看看日志:
orderExt()方法事务不生效时主要日志:
从日志我们可以看出从头到尾只有一个事务。根据我们上面的分析,我想你应该清楚为什么了。
orderExt()方法事务生效时主要日志:
通过日志可以很直观的分析出:事务生效了。所以有的时候,静下心来分析日志也是很有必要的。
最后吟诗一首:
曾经年少只懂用,头发全黑且茂密。
如今长大知原理,照镜方悔发量少。
完结撒花,下期再见,
用匠心敲代码,对每一行代码负责。长按关注,点点"在看",谢谢大家!