本篇来聊一下mybatis的缓存机制,基于3.4.6版本。
知识点
- 什么是缓存
- mybatis缓存
- 缓存实现机制
对于缓存的概念,我相信学过编程的都知道,它主要针对的是访问效率。我们的程序如果去磁盘或者远程获取资源都是有消耗的,磁盘的消耗在IO这块,远程的消耗在网络这块,这里又涉及到用户态和内核态的切换消耗,那怎么来减少这些访问呢?我们可以把经常需要访问的数据存到磁盘之后再复制一份到jvm内存里,对于这部分数据,我们直接去jvm里获取,而不用去远程或者磁盘上获取,这样就提高了程序的性能,这部分内存里的数据就叫做缓存。它本质上是一种空间换时间的性能优化方式。
mybatis缓存
mybatis是一款优秀的持久化框架,当然也有自己的缓存机制。这一点相信大家想想也能知道为什么,去数据库获取数据肯定是有一定性能损耗的,那我们就可以对于同样的sql查询操作做一些缓存,减少数据库的访问并提高数据获取效率。那么mybatis有哪些缓存并且要如何来使用呢?
缓存类型 这里先介绍一下mybatis有哪些缓存类型。mybatis分为一级缓存和二级缓存,什么是一级,什么是二级呢?
- 一级缓存
mybatis 默认开启的,是基于 SqlSession 级别的缓存,也就是说同一个session中是可以对缓存做复用的,但是不同的session中,缓存就是各管各的。引用这篇文章一幅图
文章图片
- 二级缓存
文章图片
我们在执行一个查询操作的时候,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);
看下执行结果:
文章图片
可以看到请求了两次数据库,这就符合不同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);
看下结果
文章图片
可以看到就访问了一次数据库!
我们再来试一下二级缓存,二级缓存是需要单独配置的。有两个地方要配置,第一个是全局配置文件,这个不配默认也是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的,看下结果:
文章图片
确实二级缓存生效,只访问了一次数据库。这里代码会发现两个session之间有一个
sqlSession.close();
,为什么需要这一句呢?因为sql查询一次之后mybatis只会将结果存到待提交map里,只有做了commit或者close,二级缓存才会刷入,如果没有这一步操作来刷入,则二级缓存不会生效。缓存实现机制
最后来介绍一下 mybatis 的缓存实现机制。mybatis 的缓存实现都在这个包下
文章图片
从包名我们也大概看出一些端倪,很明显
decorators
包的意思就是装饰的意思,也就是该包下的缓存实现都是使用了装饰器模式(针对的都是二级缓存),有哪些实现呢?文章图片
对于各个实现就不做介绍了,引用这篇文章一副图说明
文章图片
对于一级缓存,都是用的`PerpetualCache
文章图片
我们也可以自定义缓存实现,只要实现这个
Cache
接口,然后在配置文件中指定type即可,参考官方文档文章图片
再来看下一级缓存和二级缓存是在什么时候用起来的。
二级缓存实现 我们在打开一个session的时候,会创建一个执行器,直接看创建执行器的逻辑
文章图片
可以看到这里有个判断,这个就是二级缓存的全局启用配置,而
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)
文章图片
这里会从 MappedStatement 中获取对应的缓存对象,如果缓存对象为空,则不会用到二级缓存,来看下这个缓存对象是怎么生成的。之前在聊二级缓存使用的时候说到需要配置
cache
或cache-ref
,其实就是为了生成这个缓存对象。文章图片
这里跟进去逻辑比较简单,
cache-ref
是引用另一个mapper的namespace缓存对象,cache
是创建新的缓存对象,就不一一介绍了。回到上面的
org.apache.ibatis.executor.CachingExecutor#query
,我们会看到下面这行去取缓存文章图片
这个 tcm 是一个
org.apache.ibatis.cache.TransactionalCacheManager
类型的对象,负责对执行器下所有的二级缓存对象进行管理,本质上是用的org.apache.ibatis.cache.decorators.TransactionalCache
对缓存包了一层,这里用到了装饰器模式,添加了类似事务的功能文章图片
在
TransactionalCache
中可以看到我们存入缓存的时候是加入 entriesToAddOnCommit 变量中的,只有在调了 commit() 之后才会刷入到缓存中,也就是事务机制文章图片
二级缓存什么时候被清理呢?这就和我们的配置相关了。再来看下创建缓存对象的地方
org.apache.ibatis.mapping.CacheBuilder#build
文章图片
缓存的类型默认是
PerpetualCache
,当然你也可以通过 type 来指定,从上面的代码可以看到,会通过装饰器模式在PerpetualCache
缓存上加一层,在setStandardDecorators
方法中再加层层装饰文章图片
这里有一个
ScheduledCache
缓存类型,如果我们设置了flushInterval
则会在每次调用的时候判断缓存是否过期,过期则清理。当然在上一层装饰会生成对应的删除缓存规则的缓存类,目前有四种,分别为FifoCache、LruCache、WeakCache、SoftCache,我们可以自己配置,前面两种在缓存数量超过指定大小(默认1024)的时候删除指定缓存,后两种由引用规则来删除。一级缓存实现 当二级缓存取不到的时候,就会开始去一级缓存获取。看下
org.apache.ibatis.executor.BaseExecutor#query(...)
文章图片
一级缓存获取不到则去数据库获取
文章图片
可以看到获取到之后会存入一级缓存。
一级缓存什么时候清理呢?
- 在做更新、插入等修改操作之后会进行一次清理,
- 将mapper配置中对应 id 节点的
flushCache
设置为true,在每次查询之前判断是否有正在查的,没有则清理 - 全局的setting配置中将
localCacheScope
设置为STATEMENT,则在每次查询之后会判断是否还有正在查的,没有则清理
而对于倍乘数为什么取37,如果对超过 50,000 个英文单词(由两个不同版本的 Unix 字典合并而成)进行 hash code 运算,并使用常数 31, 33, 37, 39 和 41 作为乘子(cachekey使用37),每个常数算出的哈希值冲突数都小于7个(国外大神做的测试),那么这几个数就被作为生成hashCode值得备选乘数了。取自这篇文章。
虽然减少hash冲突提高了hashmap的存入效率,但是还是会出现hash冲突的情况,所以重写了equals,防止sql/结果集配对错误。
文章图片
总结
mybatis的缓存内容还是有不少的,主要使用到了装饰器模式进行解耦,通过以上介绍,相信大家对于mybatis的缓存都了解很深入了,我们平时开发也是可以基于二级缓存实现来设计的。
参考资料
https://www.cnblogs.com/wuzhe...
https://mybatis.org/mybatis-3/
【mybatis之缓存机制】https://blog.csdn.net/xl33793...
推荐阅读
- 框架|Mybatis的一级缓存和二级缓存
- Mybatis日志工厂
- MyBatis的功能架构是怎样的
- Mybatis入门之CRUD
- Mybatis练习(1)
- Java|MyBatis(五)——MyBatis中的缓存机制
- mybatis之脚本解析器
- mybatis|记mybatis查询null字段导致的NPE
- Mybatis 动态查询、插入、修改操作