面试题:Spring 事务失效的几种场景及原因
1、检查异常导致事务失效
(例如文件未找到这种需要去声明异常或者需要try。。catch处理的)
原因:spring只对Error和RuntimeException这两个类及其子类才会进行回滚操作,如果抛出的是检查异常并不会进行回滚。
解决:在注解上表明回滚的异常
-
抛出检查异常导致事务不能正确回滚
原因: Spring 默认只会回滚非检查异常
解法:配置rollbackFor属性
2,try…catch导致事务失效
声明式事务的实现原理:
声明式事务再容器中拿得的bean并不是我们的原始目标对象,而是代理对象,代理对象在调用相关方法的时候去调用事务通知,也就是帮我们加事务的控制(是提交还是回滚),在事务通知内部才会去调用原始的目标对象,事务通知是一个环绕通知,环绕通知内部再调你的原始目标对象的方法,所以它这个是在最里面被调用的,如果最里层方法把异常捕捉住了,那外层的事务通知,他不知道这个方法是有异常的,它就提交了事务。
异常必须抛出去,对于外层的调用者才知道。不把异常抛出去,外层的事务不知道,所以会提交事务。
解决:
1.将异常抛出去
![image-20220326193757214](https://i-blog.csdnimg.cn/blog_migrate/96a60a53a5d6f329ade8584c27233187.png)
2.告诉外层调用者回滚事务
![image-20220326193943020](https://i-blog.csdnimg.cn/blog_migrate/6610d49872dc9515c045a73b94ba4a09.png)
业务方法内自己try-catch异常导致事务不能正确回滚
原因:事务通知只有捉到了目标抛出的异常,才能进行后续的回滚处理,如果目标自己处理掉异常,事务通知无法知悉
解法1:异常原样抛出
解法2:手动设置TransactionStatus.setRollbackOnly()
3、切面顺序
切面类:
要织入的方法:
执行顺序:
先是最外层事务切面(环绕通知), 中间一层是自定义的切面,最里层是目标方法。
事务失效分析:
目标方法抛出异常,这个异常先抛给中间一层的自定义的切面。中间自定义的切面中try…catch了,没有将异常抛给外层切面事务。
解决方法和上面的2是一样的。要么将异常抛出,要么调用setRollbackOnly()方法。
解决方法三:交换事务切面和自定义切面的顺序,使用order,数字越小的优先级越高。(浓缩的都是精华)
spring的是事务切面的order为最大的整数值,也就是说优先级最低。
我们的目标方法抛出异常后,被里层的事务切面拿到,进行事务回滚。
总结:我们处理异常的时候尽量使用抛出异常的方式,让spring事务进行回滚。
- aop 切面顺序导致导致事务不能正确回滚
原因:事务切面优先级最低,但如果自定义的切面优先级和他一样,则还是自定义切面在内层,这时若自定义切面没有正确抛出异常…
解决同2
4、非public方法
@Transactional注解必须加在公共方法上,否则形同虚设
总结
5、父子容器
添加controller方法调用service
对于controller和service我们可以将它们配置在同一个容器当中,也可以将它们配置在父子容器中。
将service、mapper、数据源放在父容器当中,controller放在子容器当中。
父子容器设置完了有什么用?
子容器找某个bean的时候,比如子容器要用到service,如果子容器里面没有这个service,那它就回到父容器里面去找,完成依赖注入。
发现在controller调service方法的时候根本就没有加上事务。
原因:子容器里面包扫描的范围太大了,将service也扫描进来了,但是我们子容器里没有配置声明式事务。这个时候controller注入的是子容器里面的service。
只有在父容器里面才配置了@EnableTransactionManagement。
解决:
不要让子容器controller扫描到他不该扫描到的包,只扫描controller所在的包即可。
6、本类方法调用(传播行为失效)
传播行为:
REQUIRED:如果之前没有事务,那它就会创建新的事务。如果有了事务,那就加入已有的事务。REQUIRES_NEW:不管之前有没有事务,都会创建一个新的事务。
两个方法相互调用,bar方法会创建一个新的事务,所以应该会有两个事务。
发现只有一个事务,两个方法在一个事务中。
问题出在事务的代理上。因为事务的增强功能只有通过代理去调用这个方法,才会代理调用我们的事务通知,事务通知再调目标,才具备事务的增强。
但是在foo方法的内部,需要间接调用bar方法的时候。bar()方法不是通过代理调用的,不会进行功能的增强。
解决:
让bar()方法调用的时候也要通过代理对象去调用才可以。
1、自身注入自身(循环依赖)
2、通过AopContext(aop上下文)得到当前代理对象(要设置expose=true)
上面两种解决方法都是获取代理对象实现功能增强。
7、原子性失效
多线程下这段转帐代码是有问题的。
使用CountDownLatch进行多线程测试
先让主线程等待(latch.await()),当线程1和2都执行完了,主线程恢复向下运行。
发现一号账户转成了负数,出现了问题。
多线程下会出现指令交错,
会想到在方法上加synchronized加锁,但还是有问题。
即在方法中的操作执行完了(三条数据库操作),这个时候synchronized就会把锁释放,但是这个时候事务还没提交,此刻另外一个线程抢到了锁,在对其进行操作,就会出现问题。
解决:
1、在调用转帐方法的时候加锁,扩大锁的范围
它就不仅包括目标方法的调用,还有代理方法,以及中间事务通知包括目标方法。他们的调用都是原子的。
锁的位置不能加在目标方法上,要加就要加在代理方法的调用上
2、利用数据库加锁和事务配合使用
select不会阻塞,但是可以修改让其加锁阻塞
如果你把synchronized加载目标方法上,并不能保证原子性,因为目标方法里仅包含了sql语句的操作,最后的提交操作是在通知里完成的。加在目标方法上的synchronized并不能将通知上的提交操作也保护起来
可以修改让其加锁阻塞
[外链图片转存中…(img-3W6Re8cG-1648384749282)]
[外链图片转存中…(img-n97p7q61-1648384749283)]
如果你把synchronized加载目标方法上,并不能保证原子性,因为目标方法里仅包含了sql语句的操作,最后的提交操作是在通知里完成的。加在目标方法上的synchronized并不能将通知上的提交操作也保护起来