一、背景
前段时间和一起青训的言友找我探讨一段代码,代码简化流程如下图所示:
简述就是一个增or改的一个操作,对加锁这一操作进行了细粒度化,疑问点是查询了一次数据集判断为此操作为更新操作后又进行了一次查询。后续我推测之所以这样写是由于隔离级别修改成了 READ-COMMITTED,推断它是为了让获取锁的线程准备执行更新操作的时候拿到的数据是最新数据。经过查询确实如此:
这事过后本来没有然后了,想着在本地用docker搭建平常没事写写demo啥的需要的环境(轻量方便嘛)-Nacos、MySQL、Redis…本想写一篇一个请求过来从外到框架底层到事务之间都做了什么事情,然后的话就想到一个问题:
问题
MySQL事务隔离级别改成了读已提交,在开启Spring事务的情况下,共用的一个SqlSession,加上一级缓存的存在,俩次一样的查询不应该一定一样吗?那如果第一次查询之后记录变更了第二次查询不应该不能感知到吗?成了重复读的现象了?(后面那人跑来说他mentor说一级缓存关配置中掉了,然后我也没看,想看的话也看不着配置中心的配置,然后博客都写一半了,就继续吧)
而后测试证明了自己的质疑是正确的。
自测
调试一段代码,第一次查询是有日志,且查询结果里面的 age 是为 88:
然后我去将数据库这条记录的 age 修改成 25:
按照隔离级别为读已提交的情况下,每次读都会更新一致性视图,那现在应该读到到的 age 是 25,可是结果是 88,且是没有SQL执行日志的,说明这里是没有执行SQL的:
这就验证了在事务隔离级别为READ-COMMITTED的Spring事务下的俩次相同检索,就获取不到最新数据,也就证明了上面阐述的问题是存在的。
二、问题原因
(画了个流程图解释)
大致流程就是说:
- Spring事务开启了,代理生效了;(这里没对事务做源码解析,可以看看之前写的:透过源码看Spring事务)
- 进入代码块,到代理逻辑,数据库连接通过数据源(数据库连接池)创建了被放到了resources上下文里了;
- 执行查询获取这个resources里的数据库连接,然后执行,执行完放入一级缓存;
- cacheKey和上一个的相同,一级缓存里有结果,查完直接返回,本来如果没走缓存根据隔离级别为读已提交的话本来已经去获取最新的已经提交了的数据了。
三、如何解决?
- 直接关闭这个SQL的一级缓存,local-cache-scope 改为 statement
-
使用手动事务(比如用Spring提供的TransactionTemplate)去控制事务的范围,上面例子的话流程图可以改成下面这样也可解决:
-
中间写了更新这条记录的逻辑,会清空这个一级缓存;