源码刨析:mybatis二级缓存[954L]

13 篇文章 0 订阅
10 篇文章 0 订阅

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
这个是二级缓存的日志打印,那是不是就说明这里命中了二级缓存?
答案是错误的,这里还是命中的一级缓存,只不过打印二级缓存的日志,说明二级缓存生效了,先去二级缓存查询了一下,但是没有命中二级缓存,因为二级缓存中并没有数据,那为什么第一次查询没有写入二级缓存?继续玩下看,你就知道了!

上述例子的缓存命中流程

client 二级缓存 一级缓存 db 第一次查询 查询一级缓存 查询数据库 数据返回,写入一级缓存 数据返回,没有写入二级缓存 数据返回 第二次查询 查询一级缓存 命中缓存,返回数据 数据返回 client 二级缓存 一级缓存 db

缓存命中规则

影响二级缓存的命中规则与一级缓存列的两点一致,这里就不列了,可自己编码尝试一下。


缓存生命周期

生命周期顾名思义就是缓存何时创建,缓存的作用域,缓存何时会销毁,以及遇到什么情况会销毁

何时创建
当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也是同样的效果,这里就不做例子了,可自己尝试

二级缓存命中流程

client 二级缓存 一级缓存 db 第一次查询 查询一级缓存 查询数据库 数据返回,写入一级缓存 数据返回,执行close/commit,写入二级缓存 数据返回 第二次查询 命中缓存,返回数据 client 二级缓存 一级缓存 db

二级缓存的作用范围

  • 同一个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

源码调用过程

DefaultSqlSession
CachingExecutor
TransactionalCacheManager
TransactionalCache

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。这个就是实际的二级缓存了。
搞定!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

954L

帮帮孩子把~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值