mysql mybatis 脏读_记一次 Mybatis 一级缓存清理无效引起的源码走读

本文记录了一次关于 Mybatis 一级缓存清理无效的问题排查过程。通过源码阅读,发现 `SqlSession.clearCache()` 实际上清空了 HashMap 缓存,但在特定情况下,查询仍会返回旧值。进一步分析发现,问题在于 MySQL 的默认事务隔离级别为 "可重复读",导致脏读。解决方案是将事务隔离级别设置为 "读已提交"。
摘要由CSDN通过智能技术生成

今天对象在学习 Mybatis 时发现 org.apache.ibatis.session.SqlSession 对象的 clearCache() 方法并不能清理一级缓存, 同一 session 下相同查询条件返回的结果还是旧值。测试代码如下

183b0f28b60431663db1f9f7d20bf227.png

上网搜索

查看官方文档

SqlSession 实例有一个本地缓存在执行 update,commit,rollback 和 close 时被清理。要明确地关闭它(获取打算做更多的工作) ,你可以调用 clearCache()。

看起来, 没什么问题, 方法也没有被标记成废弃.

打印详细日志

先把日志配上, 看看有没有打印什么有用的信息, 添加 slf4j、logback 依赖,添加 logback.xml , 日志级别设置为 DEBUG 运行后未看到跟清理缓存有关的信息, 调整日志级别为 TRACE 后依旧没有.

mybatis

%d{HH:mm:ss.SSS} [%thread] %-5level %logger %msg%n

mybatis.log

UTF-8

%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n

看 clearCache() 源码

上面方法都没有收获, 只能看源码了.第一步, 先看一下 clearCache() 做了什么, 下面会大规模贴图

032493311485ed1ddfb0a05c8967bbf4.png

e51f3d57faaa491c15302f39a276f7e0.png

68b290b10491ed548c53472ec7b64367.png

注意 PerpetualCache 类的 cache 变量

13cad5d4d356aab4e850579a9d591cc3.png

org.apache.ibatis.cache.impl.PerpetualCache#cache 就是一个 HashMap

到此, clearCache() 已经完结, 最终就是调用一个 HashMap 的 clear() 方法

看 selectOne() 源码

e8bb2e6ec3fe6cda0a66a3b594d79310.png

这一步没有什么好看的, 就是封一层 selectList

46c8f434526965da495ce8335c4de3c6.png

第一个方法会间接调用第二个, 只是少了一个分页相关的 RowBounds

把传入的 statement 值变成 MappedStatement, 由于不是我们查看源码的重点, 可以直接跳过.

不过可以学习到 Mybatis 其实是把我们写的 xml 文件抽象成 MappedStatement , 在执行 sql 时需要先使用 statement (也就是我们 xml 中 select 标签中的 id) 去配置中 get 出整个 MappedStatement, MappedStatement 包含了 resultMaps 之类的, 一会儿 sql 返回时映射结果集很可能要用到.

d94c09454c073f202272f497f013dffe.png

这一步把 MappedStatement 变成 BoundSql, BoundSql 应该就是每条 SQL 的抽象.

还会根据 MappedStatement (xml 文件)、parameter (sql 参数)、rowBounds (分页信息)、BoundSql (SQL) 生成一个 CacheKey (缓存 key) .

已经跟我们想要了解的东西沾点边了.

a91759fe3a1d1daa271edcd506d2cbfa.png

这一步是取 MappedStatement 对象的 Cache , 暂时不知道是什么缓存(可能是二级缓存), 可以知道的是和刚才看 clearCache() 清理的不是同一个东西. 调试发现返回值是 null, 不关心继续往下看

241ed6ca909cdf9cb7b61b89c1db2245.png

这里到了 BaseExecutor 类, 152 行会根据 CacheKey 从 localCache 获取结果.

而且和 clearCache() 方法清理的是同一个缓存对象.

基本可以确定 Mybatis 就是在这里从一级缓存获取结果后返回, 需要重点关注.

阶段性成果

反复运行发现如下规律:

如果第二次查询前不加 sqlSession.clearCache(); 可以从 localCache get 出结果

如果第二次查询前加 sqlSession.clearCache(); localCache get 结果为空

由此可以得出结论:

sqlSession.clearCache() 方法是有效的, 清理一级缓存后第二次查询结果依然和第一次相同, 和 Mybatis 一级缓存并无关系.

既然如此, 要想知道结果, 只能继续往下跟踪, 看一级缓存为空后, Mybatis 是怎么处理的.

fcc0e34413a5d86e6f01c802dd424455.png

可以看出, 为空后调用了 queryFromDatabase 方法,从方法名可以理解, 会去数据库查询

7c5ef5b313cd56840d7fadf2b3acc3e4.png

第 322 行先往一级缓存设置一个占位符, 并无实际含义

第 324 行执行查询动作, 需要重点关注

第 326 行根据缓存 key 清理一级缓存

第 328 行重新设置一级缓存

第 330 行看到一个面熟的东西, 在 clearCache() 时会同时清理 localCache 和 localOutputParameterCache, 如果执行的是存储过程, 会把参数缓存起来

继续跟踪 doQuery 方法

d209c207b5a9a2a78438ba4872933bb2.png

先是获取 MappedStatement 的配置, 创建一个 StatementHandler, 加工成 JDBC 标准的 Statement , 这中间隐藏了无数细节, 还是那句话, 不是我们关心的重点, 继续跟踪 Query 方法

948842b0bedb191c70a457acebf16185.png

经过 RoutingStatementHandler 路由分发, 到达 SimpleStatementHandler

938f3ce6339af4c2c1cc632713912112.png

方法体只有三行

第一行拿出具体 SQL

第二行调用 statement.execute() 方法, 这里已经到了 JDBC 驱动层, mysql 驱动包会帮我们封装请求包发送给 mysql 服务器并把响应结果映射到 jdbc 规范的对象中

第三行处理返回结果集

其实执行完 execute 方法, 就可以从 PreparedStatement 对象 get 出想要的结果集, 但贸然 get 会影响 Mybatis 处理, 还是继续跟踪 handleResultSets 方法吧

d7538b5908a9b852b452eacc018e31b8.png

方法一开始声明了一个 multipleResults , 这个就是最终的结果集.

接着分别处理 ResultMap 和 ResultSet, 把 mysql 返回的结果按照 xml 中的规则映射成指定对象

由于 xml 中的 select 并没有定义 resultSets , 只关注上半部分即可, 断点设在 198 行

02a95d9cc27e7422b97f5f811f73a1f4.png

可以看出 mysql 服务器返回的确实是旧值,

阶段性成果

至此可以确定一级缓存清理无效的问题和应用没有关系.

还能是什么问题呢, 难道是事务的隔离级别导致的, 应用只是简单的查询, 连事务管理器都没有配置, 要有问题也只能怀疑 mysql 服务器.

查询数据库的默认隔离级别

mysql> SELECT @@global.tx_isolation;

+-----------------------+

| @@global.tx_isolation |

+-----------------------+

| REPEATABLE-READ |

+-----------------------+

1 row in set, 1 warning (0.00 sec)

mysql> SELECT @@session.tx_isolation;

+------------------------+

| @@session.tx_isolation |

+------------------------+

| REPEATABLE-READ |

+------------------------+

1 row in set, 1 warning (0.00 sec)

mysql> SELECT @@tx_isolation;

+-----------------+

| @@tx_isolation |

+-----------------+

| REPEATABLE-READ |

+-----------------+

1 row in set, 1 warning (0.00 sec)

竟然是"可重复读", 好了, 原因找到, 此贴终结.

解决

解决办法就是把事务的默认隔离级别设置成 "读已提交".

mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

Query OK, 0 rows affected (0.00 sec)

mysql> SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;

Query OK, 0 rows affected (0.00 sec)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值