MyBatis缓存
MyBatis包含一个非常强大的查询缓存特性,可以非常方便的配置和定义。缓存可以极大的提高查询效率
MyBatis系统中默认定义了两个级别的缓存,一级缓存和二级缓存;默认情况下,只有一级缓存开启,二级缓存的开启需要手动配置
- 一级缓存:线程级别的缓存,本地缓存、Sql Session级别的缓存
- 二级缓存:全局范围的缓存,除过当前线程,sqlSession能用外其他也可以使用 MyBatis实际是将缓存放在Map中
工程搭建
复制mybatis-dynamic-sql项目,并重命名为mybatis-cache,注意要修改pom.xml中artifactId为mybatis-cache,重新打开项目即可
一级缓存
- 一级缓存(local cache)即本地缓存,作用域默认为sqlSession,当session flush或者close后,该session中所有的cache将会被清空
- 本地缓存不能被关闭,但是可以调用clearCache()方法清空,或者改变缓存的作用域
一级缓存生效
同一会话期间只要查询过的数据都会保存在当前的sqlSession的一个Map中,Map的key为hashCode 查询的SQLId 编写的SQL语句 SQL的参数
新增测试方法testCacheWithSameSession,先后获取同一个Teacher,在同一session下
代码语言:javascript复制@Test
public void testCacheWithSameSession(){
TeacherMapper teacherMapper = openSession.getMapper(TeacherMapper.class);
Teacher teacher = teacherMapper.getTeacherById(1);
System.out.println(teacher);
System.out.println("---------------------------------------------------");
Teacher teacher1 = teacherMapper.getTeacherById(1);
System.out.println(teacher1);
}
执行测试
从控制台输出内容可以看出,只执行了一次查询SQL。
一级缓存失效的几种情况
- 不同的sqlSession对应不同的一级缓存
- 同一个sqlSession但是查询条件不同
- 同一个sqlSession两次查询期间执行了一次更新操作
- 同一个sqlSession两次查询期间手动清空了缓存
同一个sqlSession但是查询条件不同
不同的sqlSession使用不同的一级缓存,只有在同一个sqlSession期间查询到的数据会保存在这个sqlSession中,下次查询才可以从缓存中拿到
新增测试方法testCacheWithDiffSession
代码语言:javascript复制@Test
public void testCacheWithDiffSession(){
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
TeacherMapper teacherMapper1 = sqlSession1.getMapper(TeacherMapper.class);
TeacherMapper teacherMapper2 = sqlSession2.getMapper(TeacherMapper.class);
teacherMapper1.getTeacherById(1);
teacherMapper2.getTeacherById(1);
sqlSession1.close();
sqlSession2.close();
}
执行测试
控制台输出两条SQL语句
同一个sqlSession但是查询条件不同
同一个方法,不同的参数,也可能之前没有执行过没有缓存数据,还是会发新的SQL
增加测试方法testCacheWithSameSessionByDiffCondition
代码语言:javascript复制@Test
public void testCacheWithSameSessionByDiffCondition(){
SqlSession sqlSession1 = sqlSessionFactory.openSession();
TeacherMapper teacherMapper1 = sqlSession1.getMapper(TeacherMapper.class);
teacherMapper1.getTeacherById(1);
teacherMapper1.getTeacherById(2);
sqlSession1.close();
}
执行测试
控制台输出两条SQL语句
同一个sqlSession两次查询期间执行了一次更新操作
更新操作包括删除和新增,即使操作的不是要查询的数据也会把缓存清空
新增测试方法testCacheWithSameSessionAndConditionAfterUpdate()
代码语言:javascript复制@Test
public void testCacheWithSameSessionAndConditionAfterUpdate(){
SqlSession sqlSession1 = sqlSessionFactory.openSession();
TeacherMapper teacherMapper1 = sqlSession1.getMapper(TeacherMapper.class);
teacherMapper1.getTeacherById(1);
Teacher teacher3 = new Teacher();
teacher3.setId(3);
teacher3.setClassName("三年三十班");
teacherMapper1.updateTeacher(teacher3);
teacherMapper1.getTeacherById(1);
sqlSession1.close();
}
执行测试
控制台输出了两条SQL语句
同一个sqlSession两次查询期间手动清空缓存
增加测试方法testCacheWithSameSessionAndConditionAfterCleanCacheManually()
代码语言:javascript复制@Test
public void testCacheWithSameSessionAndConditionAfterCleanCacheManually(){
SqlSession sqlSession1 = sqlSessionFactory.openSession();
TeacherMapper teacherMapper1 = sqlSession1.getMapper(TeacherMapper.class);
teacherMapper1.getTeacherById(1);
// 手动清空当前SqlSession的一级缓存
sqlSession1.clearCache();
teacherMapper1.getTeacherById(1);
sqlSession1.close();
}
执行测试
控制它输出两条SQL语句
每次查询,MyBatis都会先看看缓存中有没有数据,如果没有才会发送新的SQL,每个SqlSession都有自己的一级缓存
二级缓存
- 二级缓存是全局作用域的缓存
- 二级默认不开启,需要手动配置
- MyBatis提供二级缓存接口以及实现,缓存实现要求Entity实现Serializable接口
- 二级缓存在SqlSession(一级缓存)关闭或提交后,一级缓存的数据会放到二级缓存中才会生效,
二级缓存使用步骤
全局配置文件中开启二级缓存
代码语言:javascript复制<setting name="cacheEnabled" value="true"/>
需要使用二级缓存的映射文件使用cache标签配置缓存,放在mapper标签中
代码语言:javascript复制<cache></cache>
Entity实体类接口需要实现序列化Serializable接口 ```java @Data public class Teacher implements Serializable {
private Integer id; private String teacherName; private String className; private String address; private Date birthDate;
}
代码语言:javascript复制新增测试方法
```java
@Test
public void testSecondLevelCache(){
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
TeacherMapper teacherMapper1 = sqlSession1.getMapper(TeacherMapper.class);
TeacherMapper teacherMapper2 = sqlSession2.getMapper(TeacherMapper.class);
teacherMapper1.getTeacherById(1);
// 注意:只有关闭了一级缓存,二级缓存才会生效
sqlSession1.close();
System.out.println("-------------");
teacherMapper2.getTeacherById(1);
sqlSession2.close();
}
执行测试
控制台只输出了一条SQL,且缓存命中率为0.5,也就是说第一次查询没有命中缓存,第二次查询命中了缓存,说明二级缓存生效了
二级缓存的属性
在Mapper XML中配置二级缓存的cache标签有以下这些属性
- eviction:表示缓存回收策略,默认策略为LRU
- LRU:最近最少使用,移除长时间不被使用的额对象
- FIFO:先进先出,按照对象进入缓存的顺序来移除
- SOFT:软引用,易出基于垃圾回收状态和软引用规则的对象
- WEAK:弱引用,更积极的移除基于垃圾回收状态和弱引用规则的对象
- flushInterval:刷新间隔,单位为浩渺,默认不设置
- size:引用数目,代表最多可以缓存多少个对象,太多会导致内存溢出
- readOnly:只读,可以设置tru或false
- true:只读缓存,会给所有嗲用着返回缓存对象的相同实例,因为这些对象不能被修改,有很高的性能
- false:读写缓存,会返回缓存对象的拷贝(通过系列化),会更安全但也因此损失了性能
缓存查询顺序及原理
缓存的查询顺序
在TeacherMapperTest测试类中增加一个方法testCacheQueryOrder,测试缓存的查询顺序
代码语言:javascript复制@Test
public void testCacheQueryOrder(){
SqlSession sqlSession = sqlSessionFactory.openSession();
TeacherMapper mapper = sqlSession.getMapper(TeacherMapper.class);
Teacher teacher = mapper.getTeacherById(2);
System.out.println(teacher);
sqlSession.close();
}
第一次执行测试
缓存命中率为0,并且输出了执行的SQL,说明肯定去查了二级缓存和一级缓存,二级缓存中没有数据,一级缓存中也没有数据,所以查询了数据库
在末尾增加代码,再次查询
代码语言:javascript复制SqlSession sqlSession1 = sqlSessionFactory.openSession();
TeacherMapper mapper1 = sqlSession1.getMapper(TeacherMapper.class);
Teacher teacher1 = mapper1.getTeacherById(2);
System.out.println(teacher1);
sqlSession1.close();
第二次执行测试
执行了两次查询,第一次查询一级缓存和二级缓存中都没有数据所以去查询数据库,第二次从二级缓存中查到数据,所以二级缓存命中率为0.5
在sqlSession1.close()上面增加代码两行代码,再次查询teaher并输出。
代码语言:javascript复制Teacher teacher2 = mapper1.getTeacherById(2);
System.out.println(teacher2);
第三次次执行测试
两次都是从二级缓存中获获取
在sqlSession1.close()上面增加代码,查询其他ID的数据
代码语言:javascript复制// 查询一个二级缓存中不存在的数据
// 一级缓存中如果还没有,就去查询数据库,之后放在一级缓存中
Teacher teacher3 = mapper1.getTeacherById(3);
System.out.println(teacher3);
// 这次查询会从一级缓存中查询
Teacher teacher4 = mapper1.getTeacherById(3);
System.out.println(teacher4)
第四次执行测试
缓存命中率降低,说明数据不是从二级缓存而是从一级缓存中拿的,二级缓存不存在这个数据,也说明是优先看二级缓存,二级缓存中没有才去看的一级缓存,因为如果从一级缓存中直接找到数据,就没有必要再到二级缓存中去找了,缓存命中率降低就说明了先找二级缓存再找一级缓存
总结:
- 不会出现一级缓存和二级缓存中有同一个数据的情况
- 二级缓存中的数据在一级缓存也就是SqlSession缓存关闭了才有
- 二级缓存中没有数据就会看一级缓存,一级缓存中也没有就会看数据库,数据库查完之后就会存在SqlSession中
- 任何时候都是先看二级缓存在看一级缓存,最后都没有再去查询数据库
缓存的原理
缓存相关的设置
- MyBatis全局配置文件mybatis-config.xml中的cacheEnable标签配置了二级缓存的开关,一级缓存一直是开启的
- Mapper XML文件中select标签具有useCache标签,可以设置是否使用二级缓存,一级缓存一直使用
- sql标签具有flushCache属性,增删改语句默认flushCache为true,sql执行后会清空一级缓存以及二级缓存,查询语句默认flushCache为false
- sqlSession对象具有clearCache方法,只能用来清除一级缓存
- 在某一个作用域进行了增删改操作后,默认所有的select的缓存会被清空,参考一级缓存失效的几种情况
测试是否使用二级缓存,设置select语句属性useCache="false"
代码语言:javascript复制@Test
public void testUseCacheFalse(){
SqlSession sqlSession = sqlSessionFactory.openSession();
TeacherMapper mapper = sqlSession.getMapper(TeacherMapper.class);
Teacher teacher = mapper.getTeacherById(2);
System.out.println(teacher);
sqlSession.close();
System.out.println("第二次查询,不同的SqlSession,不同的Mapper");
SqlSession sqlSession1 = sqlSessionFactory.openSession();
TeacherMapper mapper1 = sqlSession1.getMapper(TeacherMapper.class);
Teacher teacher1 = mapper1.getTeacherById(2);
System.out.println(teacher1);
System.out.println("第三次查询,与第一次同一个SqlSession, 不同的Mapper");
Teacher teacher2 = mapper1.getTeacherById(2);
System.out.println(teacher2);
sqlSession1.close();
}
执行测试
没有用到二级缓存,但是用到了一级缓存,设置useCache=false对一级缓存没有任何影响
设置useCache="true",再次执行测试
缓存命中率为0.6666,第三次查询是从二级缓存中获取的,先查了二级缓存,二级缓存中有数据,就不用再去查一级缓存
整合第三方缓存
整合EHcache
MyBatis中定义了Cache接口,整合第三方或者自定义缓存可以实现该接口
该接口提供了一些必须实现的方法,如putObject方法就是将数据放入缓存中,getObject方法就是将数据从缓存中取出等其他方法。
Ehcache是一个纯Java的进程内缓存框架,具有快速、简单的特点,是Hibernate中默认的CacheProvider。
mybatis-ehcache中帮我们写好了mybatis中Cache接口的实现
第一步首先在pom.xml文件中添加ehcache依赖和mybatis-ehcache依赖
代码语言:javascript复制<!-- https://mvnrepository.com/artifact/org.mybatis.caches/mybatis-ehcache -->
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-ehcache</artifactId>
<version>1.2.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.ehcache/ehcache -->
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>3.9.9</version>
</dependency>
第二步是在resources目录下增加ehcache缓存配置ehcache.xml
代码语言:javascript复制<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">
<!-- 磁盘保存路径 -->
<diskStore path="/Users/jingnan/Practice/mybatis-cache/ehcache" />
<defaultCache
maxElementsInMemory="1"
maxElementsOnDisk="10000000"
eternal="false"
overflowToDisk="true"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
</defaultCache>
</ehcache>
属性说明:
- diskStore:指定数据在磁盘中的存储位置。
- defaultCache:当借助CacheManager.add("demoCache")创建Cache时,EhCache便会采用指定的的管理策略
以下属性是必须的:
- maxElementsInMemory - 在内存中缓存的element的最大数目
- maxElementsOnDisk - 在磁盘上缓存的element的最大数目,若是0表示无穷大
- eternal - 设定缓存的elements是否永远不过期。如果为true,则缓存的数据始终有效,如果为false那么还要根据timeToIdleSeconds,timeToLiveSeconds判断
- overflowToDisk - 设定当内存缓存溢出的时候是否将过期的element缓存到磁盘上
以下属性是可选的:
- timeToIdleSeconds - 当缓存在EhCache中的数据前后两次访问的时间超过timeToIdleSeconds的属性取值时,这些数据便会删除,默认值是0,也就是可闲置时间无穷大
- timeToLiveSeconds - 缓存element的有效生命期,默认是0.,也就是element存活时间无穷大
- diskSpoolBufferSizeMB 这个参数设置DiskStore(磁盘缓存)的缓存区大小.默认是30MB.每个Cache都应该有自己的一个缓冲区.
- diskPersistent - 在VM重启的时候是否启用磁盘保存EhCache中的数据,默认是false。
- diskExpiryThreadIntervalSeconds - 磁盘缓存的清理线程运行间隔,默认是120秒。每个120s,相应的线程会进行一次EhCache中数据的清理工作
- memoryStoreEvictionPolicy - 当内存缓存达到最大,有新的element加入的时候, 移除缓存中element的策略。默认是LRU(最近最少使用),可选的有LFU(最不常使用)和FIFO(先进先出)
第三步是在TeaacheMapper中设置二级缓存使用ehcache,并且实体类可以不再实现序列化接口
代码语言:javascript复制<!--设置第三方的自定义缓存-->
<cache type="org.mybatis.caches.ehcache.EhcacheCache" ></cache>
新增测试方法testEhcache(),将上一个测试方法的代码拷贝过来。使用第三方缓存不再需要实体类实现序列化接口
代码语言:javascript复制@Test
public void testEhcache(){
SqlSession sqlSession = sqlSessionFactory.openSession();
TeacherMapper mapper = sqlSession.getMapper(TeacherMapper.class);
Teacher teacher = mapper.getTeacherById(2);
System.out.println(teacher);
sqlSession.close();
// System.out.println("第二次查询,不同的SqlSession,不同的Mapper");
SqlSession sqlSession1 = sqlSessionFactory.openSession();
TeacherMapper mapper1 = sqlSession1.getMapper(TeacherMapper.class);
Teacher teacher1 = mapper1.getTeacherById(2);
System.out.println(teacher1);
sqlSession1.close();
}
执行测试
设置EHcache配置文件中保存到内存中的数据为1,即只要数据量超过1就会保存磁盘上,设置0的意思保存到内存中的数据是无限大
再次执行测试
查看存放缓存的磁盘目录
其他Mapper XML中配置二级缓存可以通过引用已配置缓存的namesapce
代码语言:javascript复制<cache-ref namespace="com.citi.mapper.TeacherMapper"/>
也可以直接cache标签type属性定义
代码语言:javascript复制<cache type="org.mybatis.caches.ehcache.EhcacheCache" ></cache>