Mybatis除了通过延迟加载来提供查询效率,也可以使用缓存机制。

Mybatis中有一级缓存和二级缓存,默认情况下,Mybatis开启一级缓存,关闭二级缓存:

  • 一级缓存是SqlSession级别的缓存。在操作数据库时需要构造sqlSession对象,在对象中有一个数据结构(HashMap)用于存储缓存数据。不同的sqlSession之间的缓存数据区域(HashMap)是互相不影响的。
  • 二级缓存是mapper级别的缓存,多个SqlSession去操作同一个Mapper的sql语句,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的。

一级缓存

  1. 我们在一个 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查询的执行情况

springboot取消mybatis结果缓存 mybatis关闭二级缓存_缓存


从上图可以看到,sql查询只执行了一次。

  1. 在两次查询之间进行一次commit操作

springboot取消mybatis结果缓存 mybatis关闭二级缓存_xml_02


springboot取消mybatis结果缓存 mybatis关闭二级缓存_缓存_03


通过查看控制台可以发现,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()方法。

执行之后,出现异常如下

springboot取消mybatis结果缓存 mybatis关闭二级缓存_缓存_04


原因是:开启了二级缓存后,还需要将要缓存的pojo实现Serializable接口(序列化:内存——>硬盘),为了将缓存数据取出执行反序列化(硬盘——>内存)操作,因为二级缓存数据存储介质多种多样,不一定只存在内存中,有可能存在硬盘中,如果我们要再取这个缓存的话,就需要反序列化了。所以mybatis中的pojo都去实现Serializable接口。

注意:如果要实例化一个类,其父类及其级联属性也需要序列化

因此,我们将Student类及其级联属性Card实现Serializable接口

测试结果

springboot取消mybatis结果缓存 mybatis关闭二级缓存_二级缓存_05


从上图可以发现,sql只执行了一次,第一次缓存命中率为0,执行完session.close()之后,将对象写入缓存,则第二次缓存命中率为0.5

禁用二级缓存

我们曾在sql映射文件里写了如下代码以开启二级缓存

springboot取消mybatis结果缓存 mybatis关闭二级缓存_二级缓存_06


这意味着这个映射文件里的所有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才行

springboot取消mybatis结果缓存 mybatis关闭二级缓存_二级缓存_07

方式二(刷新缓存)

在select标签中增加属性flushCache="true",默认情况下为true,即刷新缓存,如果改成false则不会刷新。如下例

<select id="queryStudentByStuNo" resultType="Student" parameterType="int" flushCache="true">
        select * from student2 where stuno = ${value}
    </select>

则执行测试之后,控制台输出如下

springboot取消mybatis结果缓存 mybatis关闭二级缓存_xml_08


一般下执行完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();
    }

测试结果,发现值查询了一次

springboot取消mybatis结果缓存 mybatis关闭二级缓存_缓存_09


而且在D:\documents\ehcache目录下也会有相关数据:

springboot取消mybatis结果缓存 mybatis关闭二级缓存_xml_10

注: 如果其他mapper.xml需要使用缓存,可进行引用mapper

<cache-ref namespace="com.santiago.mapper.StudentMapper"/>