如何让Spring事务失效 - ZhangTory's NoteBlog - 张耀誉的笔记博客

如何让Spring事务失效

金三银四的季节面试了一些公司,有两家公司都不约而同的问到了Spring的事务在哪些情况下会失效。
我本来以为这不是什么问题,失效肯定是用法不当造成的嘛,这个我在code review中发现过同事的三种错误使用,
于是把这三种错误的使用方法回答给了面试官,面试官觉得说少了,问还有呢?
一时想不出还有什么情况回导致Spring事务失效,于是开始从原理分析......
面试后查了查这个问题,我去,好多错误简直离谱,让我真心想不到。

首先要说,面试八股文太TM讨厌了,至于为什么大家都知道。
遇到问题,不知道很正常,拿如何让Spring事务失效的问题来说,我确实想不到有什么原因回导致事务失效,但是我可以通过原理分析啊,所以不论是我面试别人,还是别人面试我,我都喜欢在遇到不知道的问题时,进行适当的引导,从原理上进行分析,或给出自己的方案,即使不是最优方案也没有关系,至少能看出你的思维方式,这样才能招到合适的人才,而不是只能背书的人才。

回到主题,先说说我在实际工作code review中发现的三个错误。
1.异常被try-catch捕获,没有异常抛出,肯定就不会rollback。
2.直接抛出Exception异常,包括try-catch后手动抛出的情况。
3.在同一个类中,没有事务的方法直接调用了有事务的方法,这个错误就需要结合Spring事务的原理来说了。

现在我们不知道还有什么原因回造成事务的失效了,所以我们就从原理入手想想。
Spring的事务分为声明式事务和编程式事务。
所谓编程式事务就是在代码中显示的写明事务的开启和提交。
而我们常用的@Transactional注解就是声明式事务。

@Transactional是通过AOP实现的,而AOP是通过动态代理实现的,所以可以简单的理解为,框架在执行数据库操作前后,帮我们做了事务的开启和提交。而private方法是不能被动态代理的,如果你在private方法上加@Transactional注解,IDEA是会直接报错的。当然protected和package-visible方法IDEA不会报错,但是并不会被进行动态代理。原因也很简单,只有public方法才能在其他类或者包中被调用,protected和package-visible限制了类的访问权限,大多数情况下动态代理不能访问。而IDEA对protected和package-visible不报错也是可以理解的,因为不排除你会在同一个包下进行动态代理。
由此可以得出第一个结论:@Transactional仅对public方法有效。

通过上面的原理,我们还可以想到在同一个类中,直接调用方法的情况,也就是我code review时发现的第三种情况。熟悉动态代理都应该知道,JDK动态代理是通过接口进行代理的,cglib是通过生成动态代理类的字节码进行代理的,所以如果要使用动态代理,必须要在类的基础上进行调用,直接调用方法是不会走代理类的。那么在同一个类中如果有需求需要调用某个方便并开启事务怎么办呢?也很简单,将类自己注入进来,调用代理类的方法即可。
结合动态代理原理,可以得出第二个结论:自身调用时事务不会生效,需要将类注入后调用代理类。

AOP在没有异常出现时,会帮我们进行事务的提交,反之在有异常出现时,会帮我们进行事务的回滚。所以如果我们手动try-catch捕获了异常,异常没有被抛出,那么AOP会认为没有出现异常,从而将事务提交,造成事务失效。这也是我code review时发现的第一种情况。
所以又可以得出第三个结论:try-catch捕获异常后,需要将异常抛出,使AOP感知到有异常的发生。

同时我们需要知道,Exception异常是一切异常的基础,异常又分为运行时异常和编译时异常。运行时异常是在程序运行中,因为错误的情况导致的异常;编译时异常在编译时就会进行检查,在代码中需要显示的throw出去。那么抛出什么异常才会回滚事务呢?这个就需要去看看Spring事务的实现了,默认rollbackFor=RuntimeException,所以如果你直接抛出Exception也是不会触发事务的回滚的。关于这一点,阿里巴巴代码规范中提到@Transactional应该显示的指定rollbackFor异常,即@Transactional(rollbackFor = Exception.class)。这也是我code review时发现的第二种情况。
所以第四个结论:需要抛出正确的异常,或指定正确的rollbackFor异常。

对于Spring事务的实现还有其他参数,也可能导致事务失效,比如事务的传播性。Spring将事务的传播性Propagation分为了7种:
REQUIRED(默认):支持当前事务,如果没有就创建一个。
SUPPORTS:支持当前事务,如果没有就以非事务运行。
MANDATORY:支持当前事务,如果没有就抛出异常。
REQUIRES_NEW:新建事务,如果当前存在事务就将当前事务挂起。
NOT_SUPPORTED:以非事务运行,如果当前存在事务就将事务挂起。
NEVER:以非事务运行,如果当前存在事务就抛出异常。
NESTED:如果当前存在事务,就在其中嵌套一个事务运行;否则按照REQUIRED执行。
如果调用的个方法设置了传播性,那么可能会出现以非事务的方式执行数据库操作,而这种情况是在编程时考虑到的,一般不会出现这种问题。
这里就引出了第五个结论:考虑调用方法的事务传播性。

然后再说说网上那些让其他的事务失效原因吧,有一些是属于历史遗留问题,也就是在long long ago,还没有SpringBoot时,我们需要自己手动搭建SpringMVC工程,就需要手动进行处理一些配置项。而SpringBoot基于约定大于配置的理念,一些需要手动配置的项已经默认开启了,所以就不存在之前SpringMVC的问题了。
比如没有加@EnableTransactionManagement注解,这个就属于历史遗留问题了,现在使用SpringBoot我们就已经约定默认开启事务了。
数据源没有配置事务管理器,这个也属于历史遗留问题。
数据库本身不支持事务,巧妇难为无米之炊,这个也好理解。
@Transactional注解所在的类必须要被spring托管,如果类不是Spring的Bean,那么Spring肯定不能对其进行代理,出现这个错误,说明连spring都不会。

所以如何让Spring事务失效?我上面的结论,反着来就对了。

添加新评论

电子邮件地址不会被公开,评论内容可能需要管理员审核后显示。