性能文章>群里分享的面试题,第一题就不会了?>

群里分享的面试题,第一题就不会了?原创

370356

你好,我是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,从一点点到亿点点,我们下篇见!

 

点赞收藏
分类:标签:
yes的练级攻略

公众号【yes的练级攻略】,专注分享后端技术

请先登录,查看5条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

JDBC PreparedStatement 字段值为null导致TBase带宽飙升的案例分析

JDBC PreparedStatement 字段值为null导致TBase带宽飙升的案例分析

随机一门技术分享之Netty

随机一门技术分享之Netty

MappedByteBuffer VS FileChannel:从内核层面对比两者的性能差异

MappedByteBuffer VS FileChannel:从内核层面对比两者的性能差异

6
5