Data Access 之 MyBatis(五)- MyBatis Cache

2022-08-19 17:11:36 浏览数 (1)

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>

0 人点赞