本篇来聊一下mybatis的缓存机制,基于3.4.6版本。

知识点

  • 什么是缓存
  • mybatis缓存
  • 缓存实现机制

什么是缓存

对于缓存的概念,我相信学过编程的都知道,它主要针对的是访问效率。我们的程序如果去磁盘或者远程获取资源都是有消耗的,磁盘的消耗在IO这块,远程的消耗在网络这块,这里又涉及到用户态和内核态的切换消耗,那怎么来减少这些访问呢?我们可以把经常需要访问的数据存到磁盘之后再复制一份到jvm内存里,对于这部分数据,我们直接去jvm里获取,而不用去远程或者磁盘上获取,这样就提高了程序的性能,这部分内存里的数据就叫做缓存。它本质上是一种空间换时间的性能优化方式。

mybatis缓存

mybatis是一款优秀的持久化框架,当然也有自己的缓存机制。这一点相信大家想想也能知道为什么,去数据库获取数据肯定是有一定性能损耗的,那我们就可以对于同样的sql查询操作做一些缓存,减少数据库的访问并提高数据获取效率。那么mybatis有哪些缓存并且要如何来使用呢?

缓存类型

这里先介绍一下mybatis有哪些缓存类型。mybatis分为一级缓存和二级缓存,什么是一级,什么是二级呢?

  • 一级缓存

    mybatis 默认开启的,是基于 SqlSession 级别的缓存,也就是说同一个session中是可以对缓存做复用的,但是不同的session中,缓存就是各管各的。引用这篇文章一幅图

image.png

  • 二级缓存

二级缓存是需要我们手动开启的,非查询类操作每次操作会清理一遍,缓存是基于 namespace 级别的(可以理解为一个mapper),多个 session 可以共用。还是引用这篇文章的图

image.png

我们在执行一个查询操作的时候,mybatis 的执行顺序是:二级缓存 -> 一级缓存 -> 数据库。

如何使用

下面我们来通过案例使用一下mybatis的缓存,看下效果。

上面说过一级缓存是默认就有的,所以我们直接用,上代码

        DefaultSqlSessionFactory sqlSessionFactory = (DefaultSqlSessionFactory)applicationContext.getBean("sqlSessionFactory");
        DefaultSqlSession sqlSession = (DefaultSqlSession)sqlSessionFactory.openSession();
        UserInfo userInfo = sqlSession.getMapper(UserInfoMapper.class).selectById(1);
        DefaultSqlSession sqlSession1 = (DefaultSqlSession)sqlSessionFactory.openSession();
        UserInfo userInfo1 = sqlSession1.getMapper(UserInfoMapper.class).selectById(1);

看下执行结果:

image.png

可以看到请求了两次数据库,这就符合不同session不共享一级缓存的情况。

再改下代码

        DefaultSqlSessionFactory sqlSessionFactory = (DefaultSqlSessionFactory)applicationContext.getBean("sqlSessionFactory");
        DefaultSqlSession sqlSession = (DefaultSqlSession)sqlSessionFactory.openSession();
        UserInfo userInfo = sqlSession.getMapper(UserInfoMapper.class).selectById(1);
        UserInfo userInfo1 = sqlSession.getMapper(UserInfoMapper.class).selectById(1);

看下结果

image.png

可以看到就访问了一次数据库!

我们再来试一下二级缓存,二级缓存是需要单独配置的。有两个地方要配置,第一个是全局配置文件,这个不配默认也是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="cacheEnabled" value="true"/>
    </settings>
</configuration>

第二个是mapper文件,只要加<cache/>标签即可(也可以是使用cache-ref来引用其他namespace的缓存),当然你可以对<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="com.example.mybatisanalyze.mapper.UserInfoMapper">
    <cache/>
    <select id="selectById" resultType="com.example.mybatisanalyze.po.UserInfo">
        select * from user_info where id = #{id}
    </select>
</mapper>

这两步配置确认没问题之后,二级缓存已经开启了,上代码

DefaultSqlSessionFactory sqlSessionFactory = (DefaultSqlSessionFactory)applicationContext.getBean("sqlSessionFactory");
        DefaultSqlSession sqlSession = (DefaultSqlSession)sqlSessionFactory.openSession();
        UserInfo userInfo = sqlSession.getMapper(UserInfoMapper.class).selectById(1);
        sqlSession.close();
        DefaultSqlSession sqlSession1 = (DefaultSqlSession)sqlSessionFactory.openSession();
        UserInfo userInfo1 = sqlSession1.getMapper(UserInfoMapper.class).selectById(1);

这里是验证二级缓存是跨session的,看下结果:

image.png

确实二级缓存生效,只访问了一次数据库。这里代码会发现两个session之间有一个sqlSession.close();,为什么需要这一句呢?因为sql查询一次之后mybatis只会将结果存到待提交map里,只有做了commit或者close,二级缓存才会刷入,如果没有这一步操作来刷入,则二级缓存不会生效。

缓存实现机制

最后来介绍一下 mybatis 的缓存实现机制。mybatis 的缓存实现都在这个包下

image.png

从包名我们也大概看出一些端倪,很明显decorators包的意思就是装饰的意思,也就是该包下的缓存实现都是使用了装饰器模式(针对的都是二级缓存),有哪些实现呢?

image.png

对于各个实现就不做介绍了,引用这篇文章一副图说明

image.png

对于一级缓存,都是用的`PerpetualCache

image.png

我们也可以自定义缓存实现,只要实现这个Cache接口,然后在配置文件中指定type即可,参考官方文档

image.png

再来看下一级缓存和二级缓存是在什么时候用起来的。

二级缓存实现

我们在打开一个session的时候,会创建一个执行器,直接看创建执行器的逻辑

image.png

可以看到这里有个判断,这个就是二级缓存的全局启用配置,而CachingExecutor就是一个装饰了二级缓存功能的执行器。再来看下查询逻辑org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)

image.png

这里会从 MappedStatement 中获取对应的缓存对象,如果缓存对象为空,则不会用到二级缓存,来看下这个缓存对象是怎么生成的。之前在聊二级缓存使用的时候说到需要配置cachecache-ref,其实就是为了生成这个缓存对象。

image.png

这里跟进去逻辑比较简单,cache-ref是引用另一个mapper的namespace缓存对象,cache是创建新的缓存对象,就不一一介绍了。

回到上面的org.apache.ibatis.executor.CachingExecutor#query,我们会看到下面这行去取缓存

image.png

这个 tcm 是一个org.apache.ibatis.cache.TransactionalCacheManager类型的对象,负责对执行器下所有的二级缓存对象进行管理,本质上是用的org.apache.ibatis.cache.decorators.TransactionalCache对缓存包了一层,这里用到了装饰器模式,添加了类似事务的功能

image.png

TransactionalCache中可以看到我们存入缓存的时候是加入 entriesToAddOnCommit 变量中的,只有在调了 commit() 之后才会刷入到缓存中,也就是事务机制

image.png

二级缓存什么时候被清理呢?</font>这就和我们的配置相关了。再来看下创建缓存对象的地方org.apache.ibatis.mapping.CacheBuilder#build

image.png

缓存的类型默认是PerpetualCache,当然你也可以通过 type 来指定,从上面的代码可以看到,会通过装饰器模式在PerpetualCache缓存上加一层,在setStandardDecorators方法中再加层层装饰

image.png

这里有一个ScheduledCache缓存类型,如果我们设置了flushInterval则会在每次调用的时候判断缓存是否过期,过期则清理。当然在上一层装饰会生成对应的删除缓存规则的缓存类,目前有四种,分别为FifoCache、LruCache、WeakCache、SoftCache,我们可以自己配置,前面两种在缓存数量超过指定大小(默认1024)的时候删除指定缓存,后两种由引用规则来删除。

一级缓存实现

当二级缓存取不到的时候,就会开始去一级缓存获取。看下

org.apache.ibatis.executor.BaseExecutor#query(...)

image.png

一级缓存获取不到则去数据库获取

image.png

可以看到获取到之后会存入一级缓存。

<font color=#F08080> 一级缓存什么时候清理呢? </font>

  • 在做更新、插入等修改操作之后会进行一次清理,
  • 将mapper配置中对应 id 节点的flushCache设置为true,在每次查询之前判断是否有正在查的,没有则清理
  • 全局的setting配置中将localCacheScope设置为STATEMENT,则在每次查询之后会判断是否还有正在查的,没有则清理

CacheKey

这个是二级缓存的key,mybatis支持动态sql,所以对应的key不能直接用String来设置,才有了CacheKey。CacheKey的设计主要是为了减少hash冲突,不同的内容是有可能产生相同的hashcode(参考hashmap实现),所以这里生成hashcode使用了multiplier来进行倍乘来减少hash冲突,初始hashcode为什么是17?17是质子数中一个“不大不小”的存在,如果你使用的是一个如2的较小质数,那么得出的乘积会在一个很小的范围,很容易造成哈希值的冲突。而如果选择一个100以上的质数,得出的哈希值会超出int的最大范围,这两种都不合适。

而对于倍乘数为什么取37,如果对超过 50,000 个英文单词(由两个不同版本的 Unix 字典合并而成)进行 hash code 运算,并使用常数 31, 33, 37, 39 和 41 作为乘子(cachekey使用37),每个常数算出的哈希值冲突数都小于7个(国外大神做的测试),那么这几个数就被作为生成hashCode值得备选乘数了。取自这篇文章

虽然减少hash冲突提高了hashmap的存入效率,但是还是会出现hash冲突的情况,所以重写了equals,防止sql/结果集配对错误。

image.png

总结

mybatis的缓存内容还是有不少的,主要使用到了装饰器模式进行解耦,通过以上介绍,相信大家对于mybatis的缓存都了解很深入了,我们平时开发也是可以基于二级缓存实现来设计的。

参考资料

https://www.cnblogs.com/wuzhe...

https://mybatis.org/mybatis-3/

https://blog.csdn.net/xl33793...


爱炒股的程序猿
50 声望4 粉丝

每天进步一点点