mybatis_cache系列
建议按顺序阅读,有一些代码沿用之前的code,与一级缓存完全一致的内容或结果就不再操作了
前言
本文主要阐述mybatis二级缓存如何使用,命中规则介绍及缓存生命周期。
最后再从源码刨析缓存创建销毁的底层实现。
基础代码还是基于之前一级缓存的代码,这里就不贴了。
Coding
首先二级缓存默认是关闭的状态,默认的二级缓存我们实际开发中基本不怎么使用。
因为它的机制可能导致缓存数据不一致等问题,在接下来的实践中也会指出
开启二级缓存
mybatis.xml
<setting name="cacheEnabled" value="true"/>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<setting name="logImpl" value="STDOUT_LOGGING" />
<setting name="cacheEnabled" value="true"/>
</settings>
<environments default="dev">
<environment id="dev">
<transactionManager type="JDBC"></transactionManager>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql:///blog_mybatis_cache"/>
<property name="username" value="root"/>
<property name="password" value="passw0rd"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mapper/UserMapper.xml"/>
</mappers>
</configuration>
UserMapper.xml
<cache/>
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="mapper.UserMapper">
<cache/>
<select id="queryById" resultType="entity.User">
SELECT * FROM user WHERE id = #{id}
</select>
</mapper>
User.java
implements Serializable
/**
* @author 954L
* @create 2020/5/27 22:44
*/
@Data
public class User implements Serializable{
private Integer id;
private String name;
private String password;
}
开启完毕,运行单元测试查看效果
@Test
public void testOne() throws IOException {
InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
User user = sqlSession.selectOne("mapper.UserMapper.queryById", 1);
log.info(JSONObject.toJSONString(user));
User user1 = sqlSession.selectOne("mapper.UserMapper.queryById", 1);
log.info(JSONObject.toJSONString(user1));
}
可以看到sql语句只打印了一次,说明第二次查询是命中了缓存,那如何区分是命中一级缓存还是二级缓存?
可以看如下这段日志输出内容:
Cache Hit Ratio [mapper.UserMapper]: 0.0
这个是二级缓存的日志打印,那是不是就说明这里命中了二级缓存?
答案是错误的,这里还是命中的一级缓存,只不过打印二级缓存的日志,说明二级缓存生效了,先去二级缓存查询了一下,但是没有命中二级缓存,因为二级缓存中并没有数据,那为什么第一次查询没有写入二级缓存?继续玩下看,你就知道了!
上述例子的缓存命中流程
缓存命中规则
影响二级缓存的命中规则与一级缓存列的两点一致,这里就不列了,可自己编码尝试一下。
缓存生命周期
生命周期顾名思义就是缓存何时创建,缓存的作用域,缓存何时会销毁,以及遇到什么情况会销毁
何时创建
当sqlSession执行select后sqlsession close或者commit后会创建二级缓存
这里与一级缓存不同,一级缓存在sqlsession close或者commit之后会清空所有一级缓存
而二级缓存是必须select之后执行sqlsession close或者commit之后才会创建二级缓存
需要注意的是:当sqlsession执行的rollback时,则不会创建二级缓存,用脚趾头想想都知道,数据都回滚了,还把数据写入缓存合理吗?
尝试一下sqlsession不close不commit对二级缓存的影响
@Test
public void testOne() throws IOException {
InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession1 = sqlSessionFactory.openSession();
User user1 = sqlSession1.selectOne("mapper.UserMapper.queryById", 1);
log.info(JSONObject.toJSONString(user1));
SqlSession sqlSession2 = sqlSessionFactory.openSession();
User user2 = sqlSession2.selectOne("mapper.UserMapper.queryById", 1);
log.info(JSONObject.toJSONString(user2));
}
可以看到这里打印了两次sql语句,这里为什么用两个sqlsession?因为二级缓存是跨sqlsession的,这个后面例子会具体说明。
主要是这里如果用同一个sqlsession,那就会命中一级缓存,这里例子主要是说明sqlsession close或者commit对二级缓存的影响。
我们猜测只有在第一个sqlsession close或者commit之后才会写入二级缓存,那是不是嘞?再来写个testDemo看看
sqlsession主动close或者commit对二级缓存的影响
@Test
public void testOne() throws IOException {
InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession1 = sqlSessionFactory.openSession();
User user1 = sqlSession1.selectOne("mapper.UserMapper.queryById", 1);
log.info(JSONObject.toJSONString(user1));
sqlSession1.close();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
User user2 = sqlSession2.selectOne("mapper.UserMapper.queryById", 1);
log.info(JSONObject.toJSONString(user2));
}
可以看到确实命中了二级缓存,那就得到结论:第一次sqlsession close之后会把结果集写入二级缓存,其他sqlsession在查询相同sql、statementId、分页参数、环境等影响缓存key的值都相同时,可以命中第一次sqlsession的产生的二级缓存
commit也是同样的效果,这里就不做例子了,可自己尝试
二级缓存命中流程
二级缓存的作用范围
- 同一个sqlsessionFactory内
- 同一个mapper.xml内
同一个sqlsessionFactory内
一级缓存只在同一个sqlsession的前提下才有效。
而二级缓存的作用范围是同一个sqlSessionFactory。
意味着在同一个sqlsessionFactory中open两个不同sqlsession可以共享缓存。
尝试一下同一个sqlsessionFactory内两个不同sqlsession对缓存是否有影响
@Test
public void testOne() throws IOException {
InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession1 = sqlSessionFactory.openSession();
User user1 = sqlSession1.selectOne("mapper.UserMapper.queryById", 1);
log.info(JSONObject.toJSONString(user1));
sqlSession1.close();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
User user2 = sqlSession2.selectOne("mapper.UserMapper.queryById", 1);
log.info(JSONObject.toJSONString(user2));
sqlSession2.close();
}
可以看到与一级缓存的差别,二级缓存中不同的sqlsession仍然可以命中缓存
为了验证不同sqlsessionFactory无法命中缓存,这里尝试一下示例
@Test
public void testOne() throws IOException {
InputStream inputStream1 = Resources.getResourceAsStream("mybatis.xml");
SqlSessionFactory sqlSessionFactory1 = new SqlSessionFactoryBuilder().build(inputStream1);
SqlSession sqlSession1 = sqlSessionFactory1.openSession();
User user1 = sqlSession1.selectOne("mapper.UserMapper.queryById", 1);
log.info(JSONObject.toJSONString(user1));
sqlSession1.close();
InputStream inputStream2 = Resources.getResourceAsStream("mybatis.xml");
SqlSessionFactory sqlSessionFactory2 = new SqlSessionFactoryBuilder().build(inputStream2);
SqlSession sqlSession2 = sqlSessionFactory2.openSession();
User user2 = sqlSession2.selectOne("mapper.UserMapper.queryById", 1);
log.info(JSONObject.toJSONString(user2));
sqlSession2.close();
}
日志比较多,主要是加载SqlSessionFactory以及与mysql连接的日志。
关键我们看sql打印日志,可以看到打印了两次sql,就能说明我们sql语句与mysql交互了两次。
也就证明在不同的SqlSessionFactory情况下,二级缓存无法命中缓存!
同一个mapper.xml内
这里得新建一个mapper接口跟xml文件
UserTestMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="mapper.UserTestMapper">
<cache/>
<select id="queryById" resultType="entity.User">
SELECT * FROM user WHERE id = #{id}
</select>
</mapper>
同样我们在这个UserTestMapper.xml中开启二级缓存,内容与UserMapper.xml都一致
现在来尝试一下不同xml,执行相同的sql是否能命中二级缓存
@Test
public void testOne() throws IOException {
InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession1 = sqlSessionFactory.openSession();
User user1 = sqlSession1.selectOne("mapper.UserMapper.queryById", 1);
log.info(JSONObject.toJSONString(user1));
sqlSession1.close();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
User user2 = sqlSession2.selectOne("mapper.UserTestMapper.queryById", 1);
log.info(JSONObject.toJSONString(user2));
}
在第一个sqlsession执行close的前提下,仍然无法命中缓存,主要原因就是因为这里操作的不是同一个mapper.xml。
而二级缓存的作用域就是处在各自的mapper.xml,这点也是很多人实际开发中并不会采用mybatis默认的二级缓存的原因,当我们业务逻辑越来越庞大,很多表跟xml文件的时候,当你在另一个xml执行了update另一个表的数据时,二级缓存就会出问题
何时销毁,以及什么情况会销毁
- 执行update/insert/delete语句
执行update/insert/delete语句
这里跟一级缓存基本类似,就不贴代码了。
额外说明一点:update/insert/delete操作会销毁当前mapper.xml的二级缓存,它不会销毁别的xml的二级缓存。
说白了就是… 不知道咋说了,算了,还是贴代码把
@Test
public void testOne() throws IOException {
InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession1 = sqlSessionFactory.openSession();
User user1 = sqlSession1.selectOne("mapper.UserMapper.queryById", 1);
log.info(JSONObject.toJSONString(user1));
sqlSession1.close();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
sqlSession2.update("mapper.UserTestMapper.queryById", 1);
sqlSession2.close();
SqlSession sqlSession3 = sqlSessionFactory.openSession();
User user2 = sqlSession3.selectOne("mapper.UserMapper.queryById", 1);
log.info(JSONObject.toJSONString(user2));
}
可以看到执行了两次sql。
第一次是user1的查询结果。
第二次是update的执行sql,别问update为啥执行的是select语句。
第三次并没有打印sql,而是从二级缓存中命中。
也就说明了我们update其他xml时候不会影响当前xml的二级缓存
源码看二级缓存
缓存创建
查询
跟之前一级缓存的流程一样,我们一步步bug跟源码走,第一步sqlSession还是走的DeefaultSqlSession实现
selectOne还是走的selectList,可以看到这步是CachingExecutor的实现,二级缓存的逻辑基本都在里头
这几行代码在一级缓存时候说过,第二行是创建缓存的cacheKey,这里需要注意的是这个key即是一级缓存的key,也是二级缓存的key。
二级缓存数据结构可以理解为是hash的结构。
key - field - value
- key:xml对象
- field:cachekey
- value:二级缓存数据
好了,继续看return里的query
这里就是二级缓存的关键性代码了,稍微解释下
ms.getCache()
首先这个ms可以看到是MappedStatement对象,这个对象对应的就是我们的xml文件。
ms.getCache();就是获取这个xml的缓存,之前我们通过实践也知道二级缓存的作用域是针对xml的,所以这里是通过这个xml对象来获取当前的二级缓存。
this.flushCacheIfRequired(ms);
这个flushCache是啥呢,之前没有提到过,这个就是在mybatis的xml的标签上的其中一个属性
它的意思是表示是否清空二级缓存,例子如下
<select id="queryById" resultType="entity.User" flushCache="true">
SELECT * FROM user WHERE id = #{id}
</select>
加上这个属性后,当执行这个select后会先清空二级缓存,再把当前的查询数据写入二级缓存
所以这里的逻辑就是如果属性flushCache为true就去执行清空二级缓存的操作。
if (ms.isUseCache() && resultHandler == null) {
ms.isUseCache() 跟flushCache同样都是针对二级缓存的属性。
它表示是否使用二级缓存,如果配置false,那当前的查询将不使用缓存。
至于resultHandler,往上可以看到默认就是传递一个null
this.ensureNoOutParams(ms, boundSql);
判断sql的参数类型,如何不等于输入型参数:IN,则抛异常
List<E> list = (List)this.tcm.getObject(cache, key);
最关键代码就是这行了
首先先看它的逻辑,如果tcm获取一个list为null,则去执行一级缓存的查询逻辑(就是我们在一级缓存跟源码的那块业务逻辑),然后把查询结果集存入tcm对象里,代码都很明白了。就这个tcm是个啥?姑且猜测它就是二级缓存的实现,继续跟代码看看
点进这个tcm是一个TransactionalCacheManager类
首先它定义了一个HashMap,所有的get跟put操作都是针对这个hashMap进行操作。
那传递的这个cache参数,上面也提到过了,它就是根据xml文件获取出来的对象。
每一个xml对象都是key,对应的value是TransactionalCache对象。先看put函数往里看这个对象是个啥
putObject进来之后对应的是TransactionalCache类里定义的一个map,这个TransactionalCache跟一级缓存一样都实现Cache接口。
那看来我们到目前为止好像都完事了?但是好像有点奇怪,可以看到put是put到hashmap里,但是getObject缺是从Cache里获取。为啥嘞?
在之前的实践过程中有提到过我们要写入二级缓存的话,sqlsession必须执行close或者commit,这里先快进一下,提前告知一下,当我们执行sqlsession的close或者commit的时候会调用这个二级缓存对象的commit函数。那先看看这个commit函数都做了什么处理
确实可以看到在调用commit之后会遍历entriesToAddOnCommit跟entriesMissedInCache,把里面所有的值都put到二级缓存的cache中,而往上翻可以看到getObject就是从这个delegate中获取。差不多理清楚了,最后再跳到二级缓存的判断入口回顾一下
如果tcm.getObject为null,就去执行后续查询的逻辑,然后执行putObject函数,这两个关键的getObject跟putObject都详细看完了,应该都能明白是怎么回事了。需要注意的是这里putObject之后实际并没有存到二级缓存,只是放到一个hashMap中,等待调用二级缓存的commit时候才会把hashmap中的数据存入二级缓存中!趁热打铁,立马来看一下是如何调用二级缓存的commit
源码调用过程
close/commit
这里就直接看close了,因为sqlsession在close之前会先去commit一下,顺便一块看了
还是DefaultSqlsession,不废话了
首先来看一下这个isCommitOrRollbackRequired,这里主要是判断当前sqlsession是不是执行回滚操作。如果为true,就回滚二级缓存的查询结果,将二级缓存的hashMap清空,先这么简单说一下,主要先来看commit
commit
这回应该就都理清了把???这里把当前xml对象对应的所有二级缓存中的成员变量hashMap中的数据全都遍历一遍写到二级缓存中
rollback
这里贴一下rollback代码过程,可以看到确实是执行了clear,但是这里要注意的话本次clear是clear二级缓存的hashMap里的内,并没有clear二级缓存对象的Cache,因为我们直接可以看到二级缓存的get都是从Cache对象中获取,而当commit之后才会把hashMap数据都put到这个Cache中,所以也就能说明rollback为什么不会影响二级缓存的命中规则,它只会回滚本次的查询结果,把当前的查询结果clear掉不放入二级缓存中,而并不会影响已有的二级缓存数据!
缓存销毁
缓存销毁只有在update时候才会去销毁,我们跟下代码看看
电外交部v额iu的骄傲无法啊我服v查完偶发按我
你肯定知道我在说啥,好的,下一步!
继续下一步!
这里稍微说下,上面也有提到过这个flushCache了,当xml的属性配置了flushCache为true的情况下,这里直接去清空二级缓存了。
但是我xml明明没有配置flushCache,更别说设置为true了。这里要插一句了,当xml执行的sql标签为update时,默认flushCache为true,也就是这里为什么为true的原因!好了,继续下一步看clear
多贴了几步,待我慢慢解释!
this.getTransactionalCache(cache).clear();
获取当前xml对应的二级缓存对象,调用clear函数
this.clearOnCommit = true;
this.entriesToAddOnCommit.clear();
clearOnCommit设置为true,然后清空hashMap。注意:这里并没有清空二级缓存的cache,只是清空的二级缓存的待存入的hashMap。
那为什么这样嘞?首先我们这里执行的是update,但执行完update之后执行sqlsession会执行commit或者其他close或者rollback也好。
如果是close或者commit,那就会调用二级缓存的commit函数(这个过程上述源码过程有),这也就是这里为什么把commit的函数也贴上了。
注意看commit里如果clearOnCommit为true,也就是在clear里设置的true。就执行delegate.clear。这个就是实际的二级缓存了。
搞定!