mybatis之缓存机制

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

  • 什么是缓存
  • mybatis缓存
  • 缓存实现机制
什么是缓存
对于缓存的概念,我相信学过编程的都知道,它主要针对的是访问效率。我们的程序如果去磁盘或者远程获取资源都是有消耗的,磁盘的消耗在IO这块,远程的消耗在网络这块,这里又涉及到用户态和内核态的切换消耗,那怎么来减少这些访问呢?我们可以把经常需要访问的数据存到磁盘之后再复制一份到jvm内存里,对于这部分数据,我们直接去jvm里获取,而不用去远程或者磁盘上获取,这样就提高了程序的性能,这部分内存里的数据就叫做缓存。它本质上是一种空间换时间的性能优化方式。
mybatis缓存
mybatis是一款优秀的持久化框架,当然也有自己的缓存机制。这一点相信大家想想也能知道为什么,去数据库获取数据肯定是有一定性能损耗的,那我们就可以对于同样的sql查询操作做一些缓存,减少数据库的访问并提高数据获取效率。那么mybatis有哪些缓存并且要如何来使用呢?
缓存类型 这里先介绍一下mybatis有哪些缓存类型。mybatis分为一级缓存和二级缓存,什么是一级,什么是二级呢?
  • 一级缓存
    mybatis 默认开启的,是基于 SqlSession 级别的缓存,也就是说同一个session中是可以对缓存做复用的,但是不同的session中,缓存就是各管各的。引用这篇文章一幅图
mybatis之缓存机制
文章图片

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

我们在执行一个查询操作的时候,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);

看下执行结果:
mybatis之缓存机制
文章图片

可以看到请求了两次数据库,这就符合不同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);

看下结果
mybatis之缓存机制
文章图片

可以看到就访问了一次数据库!
我们再来试一下二级缓存,二级缓存是需要单独配置的。有两个地方要配置,第一个是全局配置文件,这个不配默认也是true。

第二个是mapper文件,只要加标签即可(也可以是使用cache-ref来引用其他namespace的缓存),当然你可以对做一些更具体的配置,参照官方文档
select * from user_info where id = #{id}

这两步配置确认没问题之后,二级缓存已经开启了,上代码
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的,看下结果:
mybatis之缓存机制
文章图片

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

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

对于各个实现就不做介绍了,引用这篇文章一副图说明
mybatis之缓存机制
文章图片

对于一级缓存,都是用的`PerpetualCache
mybatis之缓存机制
文章图片

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

再来看下一级缓存和二级缓存是在什么时候用起来的。
二级缓存实现 我们在打开一个session的时候,会创建一个执行器,直接看创建执行器的逻辑
mybatis之缓存机制
文章图片

可以看到这里有个判断,这个就是二级缓存的全局启用配置,而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)
mybatis之缓存机制
文章图片

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

这里跟进去逻辑比较简单,cache-ref是引用另一个mapper的namespace缓存对象,cache是创建新的缓存对象,就不一一介绍了。
回到上面的org.apache.ibatis.executor.CachingExecutor#query,我们会看到下面这行去取缓存
mybatis之缓存机制
文章图片

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

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

二级缓存什么时候被清理呢?这就和我们的配置相关了。再来看下创建缓存对象的地方org.apache.ibatis.mapping.CacheBuilder#build
mybatis之缓存机制
文章图片

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

这里有一个ScheduledCache缓存类型,如果我们设置了flushInterval则会在每次调用的时候判断缓存是否过期,过期则清理。当然在上一层装饰会生成对应的删除缓存规则的缓存类,目前有四种,分别为FifoCache、LruCache、WeakCache、SoftCache,我们可以自己配置,前面两种在缓存数量超过指定大小(默认1024)的时候删除指定缓存,后两种由引用规则来删除。
一级缓存实现 当二级缓存取不到的时候,就会开始去一级缓存获取。看下
org.apache.ibatis.executor.BaseExecutor#query(...)
mybatis之缓存机制
文章图片

一级缓存获取不到则去数据库获取
mybatis之缓存机制
文章图片

可以看到获取到之后会存入一级缓存。
一级缓存什么时候清理呢?
  • 在做更新、插入等修改操作之后会进行一次清理,
  • 将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/结果集配对错误。
mybatis之缓存机制
文章图片

总结
mybatis的缓存内容还是有不少的,主要使用到了装饰器模式进行解耦,通过以上介绍,相信大家对于mybatis的缓存都了解很深入了,我们平时开发也是可以基于二级缓存实现来设计的。
参考资料
https://www.cnblogs.com/wuzhe...
https://mybatis.org/mybatis-3/
【mybatis之缓存机制】https://blog.csdn.net/xl33793...

    推荐阅读