记录Spring事务+MyBatis一级缓存的一些坑

x33g5p2x  于2022-02-20 转载在 Spring  
字(2.7k)|赞(0)|评价(0)|浏览(186)

最近在工作中使用Spring的事务踩过一些坑,抽象成一些场景,记录一下。

首先创建测试表,用于后续的演示。同时约定process_flag枚举值,0表示未处理,1表示已处理。

案例1. 在事务开启后,中途修改事务的隔离级别,不生效。

插入如下数据。

为了不让MyBatis的一级缓存影响结果,将其关闭。

数据库事务隔离级别是RR.

创建几个服务类。

创建Mapper.

逻辑很简单,就是有一个大事务在最外层A,里面分批从数据库捞取未处理的数据,处理(本例子没有)之后,新开一个事务B更新状态(不能和外层事务在一起,防止事务超大)。

在数据库级别为RR的情况下,这个demo将会死循环,原因是可重复读,A事务无法感知B事务修改了状态,在它看来数据库里的数据都是未处理的。

那么,如果修改捞取数据的方法,让它变成RC,不就可以实时读到数据库的最新状态了吗?就像这样。

答案是……不行。

原因是在事务开启之后,是不能中途修改事务隔离级别的。必须在事务开启的时候就指定,因此,需要在外层TotalService指定。

有人就会说了,很明显就应该加在这里啊,为什么会想到加在Service1的方法上呢?这是因为工作中的场景可比这个复杂多了呀,哪能一眼看穿……

为什么中途不能修改呢?我在StackOverflow上找到了一个答案 https://stackoverflow.com/questions/4940648/how-to-start-a-transaction-in-jdbc?noredirect=1&lq=1 

还有一个问题,如果是直接和MySql交互,不是使用JDBC呢?我看网上说,修改会话的事务隔离级别,会立即生效。但我在客户端试了一下,虽然级别是改了,但仍然看不到别的事务的修改……

算了,不纠结了,反正知道JDBC不支持就可以了。

案例2.org.springframework.transaction.support.TransactionSynchronizationManager类的两个方法,isSynchronizationActive和isActualTransactionActive方法的差别

对上面例子稍作修改。在TotalService的外层,加一个调用,以RC开启一个事务。

对于TotalService的实现也稍作修改,将其声明为RC级别。Service1继承TotalService的事务,不单独声明。

同时,一定要把MyBatis一级缓存的开关打开,才能复现问题。

运行之后,同样是死循环,不断地从数据库读取出相同的数据。

相信大家也能猜出来,这是MyBatis一级缓存的问题,走到了缓存里,根本没有去查数据库。

一级缓存是SqlSession级别,那也就是说,MyBatis在每次查询时,都使用了同一个SqlSession。

这就很奇怪了,明明已经声明是NOT_SUPPORTED,不支持事务,为什么还会这样?在我的认知里,MyBatis一般是事务才会使用同一个SqlSession,否则每次都是新创建的。

实则不然,在org.mybatis.spring.SqlSessionUtils#registerSessionHolder 方法中,其判断是否要将会话绑定到线程资源,使用的方法是org.springframework.transaction.support.TransactionSynchronizationManager#isSynchronizationActive,该方法并不意味着当前是在事务中,它只是告诉你是否开启了同步资源。

如果要知道当前是否在事务,需要使用另一个方法org.springframework.transaction.support.TransactionSynchronizationManager#isActualTransactionActive。

这两个方法的差别,我同样是在StackOverflow上找到的答案,https://stackoverflow.com/questions/18771296/spring-transactions-transactionsynchronizationmanager-isactualtransactionactive

可以看到,在NOT_SUPPORTED的传播中,确实是挂起了之前的事务,当前无事务,但同步资源仍然是支持的。

同步资源的判断是,只要不为null就表示支持。而NOT_SUPPORTED虽然是不支持事务,实际上仍可以看作一个空事务,其初始化的时候,会给这个变量赋值,虽然是一个空集合。


 

总的来说,把NOT_SUPPORTED看作一个空事务,它会把之前的事务挂起。与数据库交互时,已经是auto commit = true; 即不在事务中。 但其仍然是支持同步资源的。

MyBatis基于这个判断,而不是基于当前是否在事务中,对SqlSession进行了资源的同步,在同一个空事务中,使用的是同一个SqlSession. 此时如果一级缓存没有关闭,则可能受其影响,不会访问数据库,直接就返回了缓存结果。

也可以说,Spring已经保证了其事务传播的正确性,但MyBatis多此一举了……

最简单的解决方法,就是把一级缓存关闭,这样就算使用了同一个会话,也不会被影响。同时,最外层的OutterService,并不需要设置为RC。原因是在NOT_SUPPORTED的这个空事务中,数据库连接已经是自动提交了(见上图),说明不在事务里面,它总是能读到最新的。

所以以后在分析事务的传播机制过程中,一定要把与数据库的连接行为(auto commit)和资源是否同步这两个概念区分开。而针对MyBatis缓存的场景,一定要将其关闭。其他的,只要考虑数据库连接行为即可!因为框架包装到最后,执行的仍然是JDBC的实现。

关于Spring中事务传播的行为,可以参考这两篇博客:

https://blog.csdn.net/kangsa998/article/details/104932913

https://www.cnblogs.com/micrari/p/7612962.html

另外,最好关闭MyBatis的缓存,因为其行为不受开发者控制,容易出现各种奇怪的结果,将其作为一个普通ORM框架即可。可以参考:https://tech.meituan.com/2018/01/19/mybatis-cache.html

最后,这是最终可运行的示例代码。

链接: https://pan.baidu.com/s/1gBmvlbsyIIUn7lS71vkpvw?pwd=uf7g 提取码: uf7g 

相关文章

微信公众号

最新文章

更多