你好,我是yes。
在群里看到小伙伴在讨论一份面试题,我一看好家伙!密密麻麻的一堆!
不过还好不要求候选人写出来,只需要面试口述回答即可。
这类面试题其实相比寻常的八股文质量高多了,毕竟更贴合实际应用,比如第一题就贴了堆栈的信息让你说出其原由,这就比较考察平常经验的累积了。
不过有小伙伴看到第一题就不会了,今天我就先来盘盘第一题,后面如果你们想要其他题的解析,可以留言给我,我再写写。

报错重现

先来个高糊截图,不知道你们看得清不?
主要报错信息就是:Transaction rolled back because it has been marked as rollback-only
这句话其实很好理解,事务被回滚了,因为它已经被标记只能回滚
毫无疑问这是一个事务问题,如果要复现这个问题,首选我们需要两个 service(逻辑简单,仅为举例)。

AddressService

可以看到逻辑非常简单,就是一个插入,方法用事务注解标记了,不过这里的插入是会抛错的
具体我是通过把 address 实体和数据库字段格式弄成不一致,来造成抛错调用,模拟事务的失败

UserService

然后我们再来一个 UserService。
里面的逻辑也不难,先插入地址,再插入用户,并且用 try catch 包裹了地址的插入,目的是为了防止地址插入抛错影响用户的插入。
并且这个插入方法也用事务注解标注了
j接下来我们调用 UserService#insert ,会发生什么呢?
我们理下:首先 addressService#errorInvoker 会抛错,但是我们用 try catch,所以按理来说影响不到后面的逻辑,紧接着执行用户的插入,最后数据库成功插入了一条数据?
错啦!
执行的结果如下:
可以看到,我们复现了第一题的问题啦!看到这里,你是不是已经有点感觉了?但是又有点奇怪?

原理分析

我们都用 try catch包裹了会出错逻辑,为什么会影响到后面的事务提交?
可以看到,我们的案例涉及了两个 service 中的两个方法,且这两个方法都标注了@Transactional 注解
这个注解里面有事务传播机制的设置,我们没填,所以默认为 Propagation.REQUIRED
我们再看看下这个字段的注释:
REQUIRED:如果已经有事务就用当前的事务,没得话就新起一个事务。
基于这些前提,我们分析一下逻辑:
首选我们调用 UserService#insert ,由于标记了事务注解,因此已经被代理了, 我们调用了代理逻辑,默认是 事务传播级别是 Propagation.REQUIRED ,所以已经新起了一个事务。
紧接着执行 AddressService#errorInvoker ,这个方法也被 @Transactional 标记,所以也被代理了,默认事务传播级别也是 Propagation.REQUIRED,而当前已经有一个由 UserService#insert  发起的事务了,所以就用这个事务。
紧接着执行地址的插入逻辑,由于字段类型不对,插入报错,于是触发事务回滚逻辑。
但是由于是否提交事务得由外层事务决定,于是乎它只能做个标记,来设置当前事务只能回滚。
紧接着插入错误被抛出,不过被 try catch 拦截,不影响后面的逻辑,于是接着处理 userMapper.insert(user),由于没有抛错,所以顺利执行,打算提交事务。
而此时这个事务因为刚才的抛错,已经被打上了回滚标记,所以提交失败,报错的原因就是
Transaction rolled back because it has been marked as rollback-only
好了原理已经分析完毕(更具体可以看后面源码分析),那如何解决这个问题呢?

解决方案

第一个方案

addressService#errorInvoker 方法上的事务注解删了,这样抛错压根就不会影响当前事务,也符合本身要求的业务逻辑:地址插入不影响用户插入的事务提交。

第二个方案

修改  addressService#errorInvoker  的事务传播机制为:REQUIRES_NEWNESTED
REQUIRES_NEW:无论如何都新起一个事务,因此执行 AddressService#errorInvoker  时候会新起一个事务,报错的话影响的是新起的事务,跟 UserService#insert 起的事务没关系。
NESTED:如果已经有事务,则会起一个嵌套事务,嵌套事务回滚并不会影响外部事务

简单源码分析

因为用了事务注解,所以原来的 service 会被代理执行,而代理逻辑会执行到TransactionAspectSupport#invokeWithinTransaction
地址的插入抛错,所以会被 catch 到走 completeTransactionAfterThrowing 的逻辑,而其内部实际会执行下面这段方法:
注释说:我们并不会回滚,别怕,如果被标记了回滚标识,我们会回滚的。
commit 后面实际的逻辑会执行到下面这个判断,而这个参数默认的配置就是 true。
也就是说内部事务失败是否标记主事务为 rollback-only 默认为  true。
因此内部事务抛错会执行了下面这个逻辑,即:
然后这一 part 就结束了,把错误抛出来,被 try catch 捕获,紧接着执行用户的插入,后面的执行很顺利,没抛错,于是正常提交事务,但在提交的过程中查到了当前事务已经被标记成 rollback-only。
于是要执行 processRollback 方法,这里注意下参数 unexpected 是 true。
看下内部逻辑会执行回滚工作,然后就会看到抛出的那个错误了:
在这里插入图片描述
简单分析完毕,有兴趣的可以自己打下断点,这部分逻辑还是比较简单清晰的。

最后

好了,第一题分析就到这了,大家应该都理解了吧,所以 try catch 也不一定是万能的,平时使用的时候还是得看仔细了。
这其实也算是一个事务传播机制的一个实战,用起来比较隐蔽,不过了解之后还是比较简单的。
关于上面的一些别的面试题,有兴趣的可以留言,我看哪个多拿出来先写写。
我是yes,从一点点到亿点点,我们下篇见!
继续阅读
阅读原文