前言
大家好,这一篇文章是MyBatis系列的最后一篇文章,前面两篇文章:手把手带你阅读Mybatis源码(一)构造篇 和 手把手带你阅读Mybatis源码(二)执行篇,主要说明了MyBatis是如何将我们的xml配置文件构建为其内部的Configuration对象和
MappedStatement对象的,然后在第二篇我们说了构建完成后MyBatis是如何一步一步地执行我们的SQL语句并且对结果集进行封装的。
那么这篇作为MyBatis系列的最后一篇,自然是要来聊聊MyBatis中的一个不可忽视的功能,一级缓存和二级缓存。
何谓缓存?
虽然这篇说的是MyBatis的缓存,但是我希望正在学习计算机的小伙伴即使还没有使用过MyBatis框架也能看明白今天这篇文章。
缓存是什么?我来说说个人的理解,最后再上比较官方的概念。
缓存(Cache),顾名思义,有临时存储的意思。计算机中的缓存,我们可以直接理解为,存储在内存中的数据的容器,这与物理存储是有差别的,由于内存的读写速度比物理存储高出几个数量级,所以程序直接从内存中取数据和从物理硬盘中取数据的效率是不同的,所以有一些经常需要读取的数据,设计师们通常会将其放在缓存中,以便于程序对其进行读取。
但是,缓存是有代价的,刚才我们说过,缓存就是在内存中的数据的容器,一条64G的内存条,通常可以买3-4块1T-2T的机械硬盘了,所以缓存不能无节制地使用,这样成本会剧增,所以一般缓存中的数据都是需要频繁查询,但是又不常修改的数据。
而在一般业务中,查询通常会经过如下步骤。
读操作 --> 查询缓存中已经存在数据 -->如果不存在则查询数据库,如果存在则直接查询缓存-->数据库查询返回数据的同时,写入缓存中。
写操作 --> 清空缓存数据 -->写入数据库
比较官方的概念:
☞ 缓存就是数据交换的缓冲区(称作:Cache),当某一硬件要读取数据时,会首先从缓存汇总查询数据,有则直接执行,不存在时从内存中获取。由于缓存的数据比内存快的多,所以缓存的作用就是帮助硬件更快的运行。
☞ 缓存往往使用的是RAM(断电既掉的非永久存储),所以在用完后还是会把文件送到硬盘等存储器中永久存储。电脑中最大缓存就是内存条,硬盘上也有16M或者32M的缓存。
☞ 高速缓存是用来协调CPU与主存之间存取速度的差异而设置的。一般CPU工作速度高,但内存的工作速度相对较低,为了解决这个问题,通常使用高速缓存,高速缓存的存取速度介于CPU与主存之间。系统将一些CPU在最近几个时间段经常访问的内容存在高速缓存,这样就在一定程度上缓解了由于主存速度低造成的CPU“停工待料”的情况。
☞ 缓存就是把一些外存上的数据保存在内存上而已,为什么保存在内存上,我们运行的所有程序里面的变量都是存放在内存中的,所以如果想将值放入内存上,可以通过变量的方式存储。在JAVA中一些缓存一般都是通过Map集合来实现的。
MyBatis的缓存
在说MyBatis的缓存之前,先了解一下Java中的缓存一般都是怎么实现的,我们通常会使用Java中的Map,来实现缓存,所以在之后的缓存这个概念,就可以把它直接理解为一个Map,存的就是键值对。
一级缓存简介
MyBatis中的一级缓存,是默认开启且无法关闭的,一级缓存默认的作用域是一个SqlSession,解释一下,就是当SqlSession被构建了之后,缓存就存在了,只要这个SqlSession不关闭,这个缓存就会一直存在,换言之,只要SqlSession不关闭,那么这个SqlSession处理的同一条SQL就不会被调用两次,只有当会话结束了之后,这个缓存才会一并被释放。
虽说我们不能关闭一级缓存,但是作用域是可以修改的,比如可以修改为某个Mapper。
一级缓存的生命周期:
1、如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用。
2、如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是该对象仍可使用。
3、SqlSession中执行了任何一个update操作(update()、delete()、insert()) ,都会清空PerpetualCache对象的数据,但是该对象可以继续使用。
二级缓存简介
MyBatis的二级缓存是默认关闭的,如果要开启有两种方式:
1.在mybatis-config.xml中加入如下配置片段
<!-- 全局配置参数,需要时再设置 --> <settings> <!-- 开启二级缓存 默认值为true --> <setting name="cacheEnabled" value="true"/> </settings>
2.在mapper.xml中开启
<!--开启本mapper的namespace下的二级缓存--> <!-- eviction:代表的是缓存回收策略,目前MyBatis提供以下策略。 (1) LRU,最近最少使用的,一处最长时间不用的对象 (2) FIFO,先进先出,按对象进入缓存的顺序来移除他们 (3) SOFT,软引用,移除基于垃圾回收器状态和软引用规则的对象 (4) WEAK,弱引用,更积极的移除基于垃圾收集器状态和弱引用规则的对象。 这里采用的是LRU, 移除最长时间不用的对形象 flushInterval:刷新间隔时间,单位为毫秒,如果你不配置它,那么当 SQL被执行的时候才会去刷新缓存。 size:引用数目,一个正整数,代表缓存最多可以存储多少个对象,不宜设置过大。设置过大会导致内存溢出。 这里配置的是1024个对象 readOnly:只读,意味着缓存数据只能读取而不能修改,这样设置的好处是我们可以快速读取缓存,缺点是我们没有 办法修改缓存,他的默认值是false,不允许我们修改 --> <cache eviction="回收策略" type="缓存类"/>
二级缓存的作用域与一级缓存不同,一级缓存的作用域是一个SqlSession,但是二级缓存
的作用域是一个namespace,什么意思呢,你可以把它理解为一个mapper,在这个mapper中操作的所有SqlSession都可以共享这个二级缓存。但是假设有两条相同的SQL,写在不同的namespace下,那这个SQL就会被执行两次,并且产生两份value相同的缓存。
MyBatis缓存的执行流程
依旧是用前两篇的测试用例,我们从源码的角度看看缓存是如何执行的。
public static void main(String[] args) throws Exception { String resource = "mybatis.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); SqlSession sqlSession = sqlSessionFactory.openSession(); //从调用者角度来讲 与数据库打交道的对象 SqlSession DemoMapper mapper = sqlSession.getMapper(DemoMapper.class); Map<String,Object> map = new HashMap<>(); map.put("id","2121"); //执行这个方法实际上会走到invoke System.out.println(mapper.selectAll(map)); sqlSession.close(); sqlSession.commit(); }
这里会执行到query()方法:
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { //二级缓存的Cache,通过MappedStatement获取 Cache cache = ms.getCache(); if (cache != null) { //是否需要刷新缓存 //在<select>标签中也可以配置flushCache属性来设置是否查询前要刷新缓存,默认增删改刷新缓存查询不刷新 flushCacheIfRequired(ms); //判断这个mapper是否开启了二级缓存 if (ms.isUseCache() && resultHandler == null) { //不管 ensureNoOutParams(ms, boundSql); @SuppressWarnings("unchecked") //先从缓存拿 List<E> list = (List<E>) tcm.getObject(cache, key); if (list == null) { //如果缓存等于空,那么查询一级缓存 list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); //查询完毕后将数据放入二级缓存 tcm.putObject(cache, key, list); // issue #578 and #116 } //返回 return list; } } //如果二级缓存为null,那么直接查询一级缓存 return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }
可以看到首先MyBatis在查询数据时会先看看这个mapper是否开启了二级缓存,如果开启了,会先查询二级缓存,如果缓存中存在我们需要的数据,那么直接就从缓存返回数据,如果不存在,则继续往下走查询逻辑。
接着往下走,如果二级缓存不存在,那么就直接查询数据了吗?答案是否定的,二级缓存如果不存在,MyBatis会再查询一次一级缓存,接着往下看。