Mybatis除了通过延迟加载来提供查询效率,也可以使用缓存机制。
Mybatis中有一级缓存和二级缓存,默认情况下,Mybatis开启一级缓存,关闭二级缓存:
- 一级缓存是SqlSession级别的缓存。在操作数据库时需要构造sqlSession对象,在对象中有一个数据结构(HashMap)用于存储缓存数据。不同的sqlSession之间的缓存数据区域(HashMap)是互相不影响的。
- 二级缓存是mapper级别的缓存,多个SqlSession去操作同一个Mapper的sql语句,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的。
一级缓存
- 我们在一个 sqlSession 中,对 student 表根据stuno进行两次查询,查看他们发出sql语句的情况。
public static void queryStudentByStuNo(){
InputStream stream = Student.class.getClassLoader().getResourceAsStream("conf.xml");
SqlSessionFactory ssf=new SqlSessionFactoryBuilder().build(stream);
SqlSession session=ssf.openSession();
StudentMapper studentMapper = session.getMapper(StudentMapper.class);
Student student = studentMapper.queryStudentByStuNo(1);
System.out.println(student);
Student student2 = studentMapper.queryStudentByStuNo(1);
System.out.println(student2);
session.close();
}
由于这两次查询用的是同一个session对象,所以进行第二次查询时,不会再次执行sql查询,而是到缓存中查看是否有对应的结果,如果有的话把结果返回,没有的话去执行查询。
我们可以通过控制台的输出来查看sql查询的执行情况
从上图可以看到,sql查询只执行了一次。
- 在两次查询之间进行一次commit操作
通过查看控制台可以发现,sql查询执行了两次。
如果中间sqlSession去执行commit操作(执行插入、更新、删除),则会清空SqlSession中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。
二级缓存
二级缓存的原理和一级缓存原理一样,第一次查询,会将数据放入缓存中,然后第二次查询则会直接去缓存中取。但是一级缓存是基于 sqlSession 的,而 二级缓存是基于 mapper文件的namespace的,也就是说多个sqlSession可以共享一个mapper中的二级缓存区域,并且如果两个mapper的namespace相同,即使是两个mapper,那么这两个mapper中执行sql查询到的数据也将存在相同的二级缓存区域中。
Mybatis中,一级缓存默认开启,二级缓存默认关闭,需要手动打开。
开启二级缓存
步骤1. 在conf.xml里进行配置
<settings>
<!--开启二级缓存-->
<setting name="cacheEnabled" value="true"/>
</settings>
步骤2. 在对应mapper.xml里声明
<mapper namespace="com.santiago.mapper.StudentMapper">
<!--声明此namespace开启二级缓存-->
<cache/>
...
</mapper>
使用二级缓存
二级缓存开启后,接下来,我们来测试一下
public static void queryStudentByStuNo(){
InputStream stream = Student.class.getClassLoader().getResourceAsStream("conf.xml");
SqlSessionFactory ssf=new SqlSessionFactoryBuilder().build(stream);
SqlSession session1=ssf.openSession();
StudentMapper studentMapper = session1.getMapper(StudentMapper.class);
Student student = studentMapper.queryStudentByStuNo(1);
System.out.println(student);
session1.close();
SqlSession session2=ssf.openSession();
StudentMapper studentMapper2 = session2.getMapper(StudentMapper.class);
Student student2 = studentMapper2.queryStudentByStuNo(1);
System.out.println(student2);
session2.close();
}
触发将对象写入二级缓存的时机:SqlSession对象的close()方法。
执行之后,出现异常如下
原因是:开启了二级缓存后,还需要将要缓存的pojo实现Serializable接口(序列化:内存——>硬盘),为了将缓存数据取出执行反序列化(硬盘——>内存)操作,因为二级缓存数据存储介质多种多样,不一定只存在内存中,有可能存在硬盘中,如果我们要再取这个缓存的话,就需要反序列化了。所以mybatis中的pojo都去实现Serializable接口。
注意:如果要实例化一个类,其父类及其级联属性也需要序列化
因此,我们将Student类及其级联属性Card实现Serializable接口
测试结果
从上图可以发现,sql只执行了一次,第一次缓存命中率为0,执行完session.close()之后,将对象写入缓存,则第二次缓存命中率为0.5
禁用二级缓存
我们曾在sql映射文件里写了如下代码以开启二级缓存
这意味着这个映射文件里的所有sql语句均开启了二级缓存,如果某个sql操作需要禁用二级缓存,只需在其标签内添加useCache="false"
这种情况是针对每次查询都需要最新的数据sql,要设置成useCache=false,禁用二级缓存,直接从数据库中获取。
<select id="queryStudentByStuNo" resultType="Student" parameterType="int" useCache="false">
select * from student2 where stuno = ${value}
</select>
清理二级缓存
方式一
与清理一级缓存的方式一样,只需调用SqlSession对象的commit()方法即可。
由于执行增删改是需要用到commit()方法,所以在执行增删改时会清理掉一级缓存和二级缓存。
注意:调用session自身的commit时不会清理二级缓存,如下方式就不会清理二级缓存,必须是增删改执行的commit才行
方式二(刷新缓存)
在select标签中增加属性flushCache="true"
,默认情况下为true,即刷新缓存,如果改成false则不会刷新。如下例
<select id="queryStudentByStuNo" resultType="Student" parameterType="int" flushCache="true">
select * from student2 where stuno = ${value}
</select>
则执行测试之后,控制台输出如下
一般下执行完commit操作都需要刷新缓存,flushCache=true表示刷新缓存,这样可以避免数据库脏读。
整合第三方缓存
MyBatis缓存做的并不专业,用的是map,但是它给了我们一个接口Cache,我们通过实现这个接口,可以自定义缓存。本例子用的为ehcache ,Hibernate用的也是ehcache缓存技术。
步骤1. 导入第三方jar包
整合ehcache二级缓存,需要导入的jar包有
ehcache-core-2.6.6.jar
slf4j-api-1.7.25.jar
mybatis-ehcache-1.1.0.jar
使用maven导入
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>2.6.6</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mybatis.caches/mybatis-ehcache -->
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-ehcache</artifactId>
<version>1.1.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
步骤2. 编写ehcache配置文件ehcache.xml
编写ehcache配置文件ehcache.xml,对其属性进行全局默认配置
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">
<!--
属性说明:
l diskStore:当内存中不够存储时,存储到指定数据在磁盘中的存储位置。
l defaultCache:当借助CacheManager.add("demoCache")创建Cache时,EhCache便会采用<defalutCache/>指定的的管理策略
以下属性是必须的:
l maxElementsInMemory - 在内存中缓存的element的最大数目
l maxElementsOnDisk - 在磁盘上缓存的element的最大数目,若是0表示无穷大
l eternal - 设定缓存的elements是否永远不过期。如果为true,则缓存的数据始终有效,如果为false那么还要根据timeToIdleSeconds,timeToLiveSeconds判断
l overflowToDisk - 设定当内存缓存溢出的时候是否将过期的element缓存到磁盘上
以下属性是可选的:
l timeToIdleSeconds - 当缓存在EhCache中的数据前后两次访问的时间超过timeToIdleSeconds的属性取值时,这些数据便会删除,默认值是0,也就是可闲置时间无穷大
l timeToLiveSeconds - 缓存element的有效生命期,默认是0.,也就是element存活时间无穷大
diskSpoolBufferSizeMB 这个参数设置DiskStore(磁盘缓存)的缓存区大小.默认是30MB.每个Cache都应该有自己的一个缓冲区.
l diskPersistent - 在VM重启的时候是否启用磁盘保存EhCache中的数据,默认是false。
l diskExpiryThreadIntervalSeconds - 磁盘缓存的清理线程运行间隔,默认是120秒。每个120s,相应的线程会进行一次EhCache中数据的清理工作
l memoryStoreEvictionPolicy - 当内存缓存达到最大,有新的element加入的时候, 移除缓存中element的策略。默认是LRU(最近最少使用),可选的有LFU(最不常使用)和FIFO(先进先出)
-->
<!-- 磁盘保存路径 -->
<diskStore path="D:\documents\ehcache" />
<defaultCache
maxElementsInMemory="1000"
maxElementsOnDisk="10000000"
eternal="false"
overflowToDisk="true"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
</defaultCache>
</ehcache>
步骤3. 在SQL映射文件中声明使用第三方缓存
<!--使用第三方缓存,可以对全局配置的属性值进行修改-->
<cache type="org.mybatis.caches.ehcache.EhcacheCache">
<property name="maxElementsInMemory" value="2000"/>
</cache>
步骤4. 测试
SQL映射文件
<select id="queryStudentByStuNo" resultType="Student" parameterType="int">
select * from student2 where stuno = ${value}
</select>
测试类
public static void queryStudentByStuNo(){
InputStream stream = Student.class.getClassLoader().getResourceAsStream("conf.xml");
SqlSessionFactory ssf=new SqlSessionFactoryBuilder().build(stream);
SqlSession session1=ssf.openSession();
StudentMapper studentMapper = session1.getMapper(StudentMapper.class);
Student student = studentMapper.queryStudentByStuNo(1);
System.out.println(student);
session1.close();
SqlSession session2=ssf.openSession();
StudentMapper studentMapper2 = session2.getMapper(StudentMapper.class);
Student student2 = studentMapper2.queryStudentByStuNo(1);
System.out.println(student2);
session2.close();
}
测试结果,发现值查询了一次
而且在D:\documents\ehcache目录下也会有相关数据:
注: 如果其他mapper.xml需要使用缓存,可进行引用mapper
<cache-ref namespace="com.santiago.mapper.StudentMapper"/>