前言
在计算机的世界中,缓存无处不在,操做系统有操做系统的缓存,数据库也会有数据库的缓存,各类中间件如Redis也是用来充当缓存的做用,编程语言中又能够利用内存来做为缓存。天然的,做为一款优秀的ORM框架,MyBatis中又岂能少得了缓存,那么本文的目的就是带领你们一块儿探究一下MyBatis的缓存是如何实现的,只需给我五分钟,带你完全掌握MyBatis的缓存工做原理。java
为何要缓存
在计算机的世界中,CPU的处理速度可谓是身先士卒,远远甩开了其余操做,尤为是I/O操做,除了那种CPU密集型的系统,其他大部分的业务系统性能瓶颈最后或多或少都会出如今I/O操做上,因此为了减小磁盘的I/O次数,那么缓存是必不可少的,经过缓存的使用咱们能够大大减小I/O操做次数,从而在必定程度上弥补了I/O操做和CPU处理速度之间的鸿沟。而在咱们ORM框架中引入缓存的目的就是为了减小读取数据库的次数,从而提高查询的效率。程序员
MyBatis缓存
MyBatis中的缓存相关类都在cache包下面,并且定义了一个顶级接口Cache,默认只有一个实现类PerpetualCache,PerpetualCache中是内部维护了一个HashMap来实现缓存。面试
下图就是MyBatis中缓存相关类:redis
须要注意的是decorators包下面的全部类也实现了Cache接口,那么为何我仍是要说Cache只有一个实现类呢?其实看名字就知道了,这个包里面所有是装饰器,也就是说这实际上是装饰器模式的一种实现。算法
咱们随意打开一个装饰器:sql
能够看到,最终都是调用了delegate来实现,只是将部分功能作了加强,其自己都须要依赖Cache的惟一实现类PerpetualCache(由于装饰器内须要传入Cache对象,故而只能传入PerpetualCache对象,由于接口是没法直接new出来传进去的)。数据库
在MyBatis中存在两种缓存,即一级缓存和二级缓存。apache
一级缓存
一级缓存也叫本地缓存,在MyBatis中,一级缓存是在会话(SqlSession)层面实现的,这就说明一级缓存做用范围只能在同一个SqlSession中,跨SqlSession是无效的。编程
MyBatis中一级缓存是默认开启的,不须要任何配置。咱们先来看一个例子验证一下一级缓存是否是真的存在,做用范围又是否是真的只是对同一个SqlSession有效。后端
一级缓存真的存在吗
package com.lonelyWolf.mybatis; import com.lonelyWolf.mybatis.mapper.UserAddressMapper; import com.lonelyWolf.mybatis.mapper.UserMapper; import com.lonelyWolf.mybatis.model.LwUser; import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import java.io.IOException; import java.io.InputStream; import java.util.List; public class TestMyBatisCache { public static void main(String[] args) throws IOException { String resource = "mybatis-config.xml"; //读取mybatis-config配置文件 InputStream inputStream = Resources.getResourceAsStream(resource); //建立SqlSessionFactory对象 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); //建立SqlSession对象 SqlSession session = sqlSessionFactory.openSession(); UserMapper userMapper = session.getMapper(UserMapper.class); List<LwUser> userList = userMapper.selectUserAndJob(); List<LwUser> userList2 = userMapper.selectUserAndJob(); } }
执行后,输出结果以下:
咱们能够看到,sql语句只打印了一次,这就说明第2次用到了缓存,这也足以证实一级缓存确实是存在的并且默认就是是开启的。
一级缓存做用范围
如今咱们再来验证一下一级缓存是否真的只对同一个SqlSession有效,咱们对上面的示例代码进行以下改变:
SqlSession session1 = sqlSessionFactory.openSession(); SqlSession session2 = sqlSessionFactory.openSession(); UserMapper userMapper1 = session1.getMapper(UserMapper.class); UserMapper userMapper2 = session2.getMapper(UserMapper.class); List<LwUser> userList = userMapper1.selectUserAndJob(); List<LwUser> userList2 = userMapper2.selectUserAndJob();
这时候再次运行,输出结果以下:
能够看到,打印了2次,没有用到缓存,也就是不一样SqlSession中不能共享一级缓存。
一级缓存原理分析
首先让咱们来想想,既然一级缓存的做用域只对同一个SqlSession有效,那么一级缓存应该存储在哪里比较合适是呢?
是的,天然是存储在SqlSession内是最合适的,那咱们来看看SqlSession的惟一实现类DefaultSqlSession:
DefaultSqlSession中只有5个成员属性,后面3个不用说,确定不可能用来存储缓存,而后Configuration又是一个全局的配置文件,也不合适存储一级缓存,这么看来就只有Executor比较合适了,由于咱们知道,SqlSession只提供对外接口,实际执行sql的就是Executor。
既然这样,那咱们就进去看看Executor的实现类BaseExecutor:
看到果真有一个localCache。而上面咱们有提到PerpetualCache内缓存是用一个HashMap来存储缓存的,那么接下来你们确定就有如下问题:
-
缓存是何时建立的?
-
缓存的key是怎么定义的?
-
缓存在什么时候使用
-
缓存在何时会失效?
接下来就让咱们逐一分析
一、一级缓存CacheKey的构成
既然缓存那么确定是针对的查询语句,一级缓存的建立就是在BaseExecutor中的query方法内建立的:
createCacheKey这个方法的代码就不贴了,在这里我总结了一下CacheKey的组成,CacheKey主要是由如下6部分组成
-
一、将Statement中的id添加到CacheKey对象中的updateList属性
-
二、将offset(分页偏移量)添加到CacheKey对象中的updateList属性(若是没有分页则默认0)
-
三、将limit(每页显示的条数)添加到CacheKey对象中的updateList属性(若是没有分页则默认Integer.MAX_VALUE)
-
四、将sql语句(包括占位符?)添加到CacheKey对象中的updateList属性
-
五、循环用户传入的参数,并将每一个参数添加到CacheKey对象中的updateList属性
-
六、若是有配置Environment,则将Environment中的id添加到CacheKey对象中的updateList属性
二、一级缓存的使用
建立完CacheKey以后,咱们继续进入query方法:
能够看到,在查询以前就会去localCache中根据CacheKey对象来获取缓存,获取不到才会调用后面的queryFromDatabase方法
三、一级缓存的建立
queryFromDatabase方法中会将查询获得的结果存储到localCache中
四、一级缓存何时会被清除
一级缓存的清除主要有如下两个地方:
-
一、就是获取缓存以前会先进行判断用户是否配置了flushCache=true属性(参考一级缓存的建立代码截图),若是配置了则会清除一级缓存。
-
二、MyBatis全局配置属性localCacheScope配置为Statement时,那么完成一次查询就会清除缓存。
-
三、在执行commit,rollback,update方法时会清空一级缓存。
PS:利用插件咱们也能够本身去将缓存清除,后面咱们会介绍插件相关知识。
二级缓存
一级缓存由于只能在同一个SqlSession中共享,因此会存在一个问题,在分布式或者多线程的环境下,不一样会话之间对于相同的数据可能会产生不一样的结果,由于跨会话修改了数据是不能互相感知的,因此就有可能存在脏数据的问题,正由于一级缓存存在这种不足,因此咱们须要一种做用域更大的缓存,这就是二级缓存。
二级缓存的做用范围
一级缓存做用域是SqlSession级别,因此它存储的SqlSession中的BaseExecutor之中,可是二级缓存目的就是要实现做用范围更广,那确定是要实现跨会话共享的,在MyBatis中二级缓存的做用域是namespace,也就是做用范围是同一个命名空间,因此很显然二级缓存是须要存储在SqlSession以外的,那么二级缓存应该存储在哪里合适呢?
在MyBatis中为了实现二级缓存,专门用了一个装饰器来维护,这就是咱们上一篇文章介绍Executor时还留下的没有介绍的一个对象:CachingExecutor。
如何开启二级缓存
二级缓存相关的配置有三个地方:一、mybatis-config中有一个全局配置属性,这个不配置也行,由于默认就是true。
<setting name="cacheEnabled" value="true"/>
想详细了解mybatis-config的能够点击这里。二、在Mapper映射文件内须要配置缓存标签:
<cache/> 或 <cache-ref namespace="com.lonelyWolf.mybatis.mapper.UserAddressMapper"/>
想详细了解Mapper映射的全部标签属性配置能够点击这里。三、在select查询语句标签上配置useCache属性,以下:
<select id="selectUserAndJob" resultMap="JobResultMap2" useCache="true"> select * from lw_user </select>
以上配置第1点是默认开启的,也就是说咱们只要配置第2点就能够打开二级缓存了,而第3点是当咱们须要针对某一条语句来配置二级缓存时候则可使用。
不过开启二级缓存的时候有两点须要注意:一、须要commit事务以后才会生效 二、若是使用的是默认缓存,那么结果集对象须要实现序列化接口(Serializable)
若是不实现序列化接口则会报以下错误:
接下来咱们经过一个例子来验证一下二级缓存的存在,仍是用上面一级缓存的例子进行以下改造:
SqlSession session1 = sqlSessionFactory.openSession(); UserMapper userMapper1 = session1.getMapper(UserMapper.class); List<LwUser> userList = userMapper1.selectUserAndJob(); session1.commit();//注意这里须要commit,不然缓存不会生效 SqlSession session2 = sqlSessionFactory.openSession(); UserMapper userMapper2 = session2.getMapper(UserMapper.class); List<LwUser> userList2 = userMapper2.selectUserAndJob();
而后UserMapper.xml映射文件中,新增以下配置:
<cache/>
运行代码,输出以下结果:
上面输出结果中只输出了一次sql,说明用到了缓存,而由于咱们是跨会话的,因此确定就是二级缓存生效了。
二级缓存原理分析
上面咱们提到二级缓存是经过CachingExecutor对象来实现的,那么就让咱们先来看看这个对象:
咱们看到CachingExecutor中只有2个属性,第1个属性不用说了,由于CachingExecutor自己就是Executor的包装器,因此属性TransactionalCacheManager确定就是用来管理二级缓存的,咱们再进去看看TransactionalCacheManager对象是如何管理缓存的:
TransactionalCacheManager内部很是简单,也是维护了一个HashMap来存储缓存。HashMap中的value是一个TransactionalCache对象,继承了Cache。
注意上面有一个属性是临时存储二级缓存的,为何要有这个属性,咱们下面会解释。
一、二级缓存的建立和使用
咱们在读取mybatis-config全局配置文件的时候会根据咱们配置的Executor类型来建立对应的三种Executor中的一种,而后若是咱们开启了二级缓存以后,只要开启(全局配置文件中配置为true)就会使用CachingExecutor来对咱们的三种基本Executor进行包装,即便Mapper.xml映射文件没有开启也会进行包装。
接下来咱们看看CachingExecutor中的query方法:
上面方法大体通过以下流程:
-
一、建立一级缓存的CacheKey
-
二、获取二级缓存
-
三、若是没有获取到二级缓存则执行被包装的Executor对象中的query方法,此时会走一级缓存中的流程。
-
四、查询到结果以后将结果进行缓存。
须要注意的是在事务提交以前,并不会真正存储到二级缓存,而是先存储到一个临时属性,等事务提交以后才会真正存储到二级缓存。这么作的目的就是防止脏读。由于假如你在一个事务中修改了数据,而后去查询,这时候直接缓存了,那么假如事务回滚了呢?因此这里会先临时存储一下。因此咱们看一下commit方法:
二级缓存如何进行包装
最开始咱们提到了一些缓存的包装类,这些都到底有什么用呢?在回答这个问题以前,咱们先断点一下看看获取到的二级缓存长啥样:
从上面能够看到,通过了层层包装,从内到外一次通过以下包装:
-
一、PerpetualCache:第一层缓存,这个是缓存的惟一实现类,确定须要。
-
二、LruCache:二级缓存淘汰机制之一。由于咱们配置的默认机制,而默认就是LRU算法淘汰机制。淘汰机制总共有4中,咱们能够本身进行手动配置。
-
三、SerializedCache:序列化缓存。这就是为何开启了默认二级缓存咱们的结果集对象须要实现序列化接口。
-
四、LoggingCache:日志缓存。
-
五、SynchronizedCache:同步缓存机制。这个是为了保证多线程机制下的线程安全性。
下面就是MyBatis中全部缓存的包装汇总:
二级缓存应该开启吗
既然一级缓存默认是开启的,而二级缓存是须要咱们手动开启的,那么咱们何时应该开启二级缓存呢?
一、由于全部的update操做(insert,delete,uptede)都会触发缓存的刷新,从而致使二级缓存失效,因此二级缓存适合在读多写少的场景中开启。
二、由于二级缓存针对的是同一个namespace,因此建议是在单表操做的Mapper中使用,或者是在相关表的Mapper文件中共享同一个缓存。
自定义缓存
一级缓存可能存在脏读状况,那么二级缓存是否也可能存在呢?
是的,默认的二级缓存毕竟也是存储在本地缓存,因此对于微服务下是可能出现脏读的状况的,因此这时候咱们可能会须要自定义缓存,好比利用redis来存储缓存,而不是存储在本地内存当中。
MyBatis官方提供的第三方缓存
MyBatis官方也提供了一些第三方缓存的支持,如:encache和redis。下面咱们以redis为例来演示一下:引入pom文件:
<dependency> <groupId>org.mybatis.caches</groupId> <artifactId>mybatis-redis</artifactId> <version>1.0.0-beta2</version> </dependency>
而后缓存配置以下:
<cache type="org.mybatis.caches.redis.RedisCache"></cache>
而后在默认的resource路径下新建一个redis.properties文件:
host=localhost port=6379 12
而后执行上面的示例,查看Cache,已经被Redis包装:
本身实现二级缓存
若是要实现一个本身的缓存的话,那么咱们只须要新建一个类实现Cache接口就行了,而后重写其中的方法,以下:
package com.lonelyWolf.mybatis.cache; import org.apache.ibatis.cache.Cache; public class MyCache implements Cache { @Override public String getId() { return null; } @Override public void putObject(Object o, Object o1) { } @Override public Object getObject(Object o) { return null; } @Override public Object removeObject(Object o) { return null; } @Override public void clear() { } @Override public int getSize() { return 0; } }
上面自定义的缓存中,咱们只须要在对应方法,如putObject方法,咱们把缓存存到咱们想存的地方就好了,方法所有重写以后,而后配置的时候type配上咱们本身的类就能够实现了,在这里咱们就不作演示了
总结
本文主要分析了MyBatis的缓存是如何实现的,而且分别演示了一级缓存和二级缓存,并分析了一级缓存和二级缓存所存在的问题,最后也介绍了如何使用第三方缓存和如何自定义咱们本身的缓存,经过本文,我想你们应该能够完全掌握MyBatis的缓存工做原理了。