MyBatis缓存

2023-12-14 16:17:36

MyBatis缓存有一级缓存和二级缓存。

  • 一级缓存是SqlSession级别,同一个SqlSession查询的数据会被缓存,下次相同的查询,会直接从缓存读取数据,不会重新查询数据库。
  • 二级缓存是SqlSessionFactory级别,通过同一个SqlSessionFactory创建的SqlSession查询的结果会被缓存;此后若再次执行相同的查询语句,结果就会从缓存中获取。
  • 默认开启的缓存,即SqlSession级别的缓存

1、SqlSession缓存

@Test
public void testCache01(){
    SqlSession sqlSession = SqlSessionUtil.getSqlSession();
    CacheMapper cacheMapper = sqlSession.getMapper(CacheMapper.class);
    // 同一个session第一次执行查询,走数据库
    Emp emp = cacheMapper.selectEmpById(1);
    System.out.println(emp);
    // 同一个session第二次执行查询,不走数据库,从一级缓存即SqlSession级别的缓存中读取结果
    Emp emp2 = cacheMapper.selectEmpById(1);
    System.out.println(emp2);
}
Emp selectEmpById(@Param("empId")Integer empId);
<select id="selectEmpById" resultType="Emp">
    select * from t_emp where id = #{empId}
</select>

执行后可以看到控制台输出如下:

 DEBUG 12-06 22:34:41,564 ==>  Preparing: select * from t_emp where id = ? (BaseJdbcLogger.java:135)
 DEBUG 12-06 22:34:41,652 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:135)
 DEBUG 12-06 22:34:42,174 <==      Total: 1 (BaseJdbcLogger.java:135)
 Emp{id=1, userName='张三丰', passWord='123', sex='男', deptId=1}
 Emp{id=1, userName='张三丰', passWord='123', sex='男', deptId=1}
  • 一级缓存失效的情况
    • 不同的SqlSession对应不同的一级缓存
    • 同一个SqlSession但是查询条件不同
    • 同一个SqlSession两次查询期间执行了任何一次增删改操作
    • 同一个SqlSession两次查询期间手动清空了缓存
1.1 缓存失效情形一
/**
 * 缓存失效情形:
 * 同一个sqlSession,中间执行了增删改操作,会使缓存失效
 * 同一个sqlSession,不同的查询条件,缓存失效
 * 同一个sqlSession,手动清除缓存
 *
 * DEBUG 12-06 22:44:14,441 ==>  Preparing: select * from t_emp where id = ? (BaseJdbcLogger.java:135)
 * DEBUG 12-06 22:44:14,607 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:135)
 * DEBUG 12-06 22:44:14,735 <==      Total: 1 (BaseJdbcLogger.java:135)
 * Emp{id=1, userName='张三丰', passWord='123', sex='男', deptId=1}
 * DEBUG 12-06 22:44:14,758 ==>  Preparing: update t_emp set pass_word = "456" where id = ? (BaseJdbcLogger.java:135)
 * DEBUG 12-06 22:44:14,759 ==> Parameters: 2(Integer) (BaseJdbcLogger.java:135)
 * DEBUG 12-06 22:44:15,116 <==    Updates: 1 (BaseJdbcLogger.java:135)
 * 1
 * DEBUG 12-06 22:44:15,118 ==>  Preparing: select * from t_emp where id = ? (BaseJdbcLogger.java:135)
 * DEBUG 12-06 22:44:15,119 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:135)
 * DEBUG 12-06 22:44:15,157 <==      Total: 1 (BaseJdbcLogger.java:135)
 * Emp{id=1, userName='张三丰', passWord='123', sex='男', deptId=1}
 */
@Test
public void testCache02(){
    SqlSession sqlSession = SqlSessionUtil.getSqlSession();
    CacheMapper cacheMapper = sqlSession.getMapper(CacheMapper.class);
    // 同一个session第一次执行查询,走数据库
    Emp emp = cacheMapper.selectEmpById(1);
    System.out.println(emp);

    // 中间进行了一次更新操作,增删改操作均会使得缓存失效
//        int i = cacheMapper.updateEmpById(2);
//        System.out.println(i);
    // 同一个session,不同的查询条件,会使缓存失效
//        Emp emp1 = cacheMapper.selectEmpById(2);
//        System.out.println(emp1);
    // 手动清除缓存
    sqlSession.clearCache();;

    // 同一个session第二次执行查询,不走数据库,从一级缓存即SqlSession级别的缓存中读取结果
    Emp emp2 = cacheMapper.selectEmpById(1);
    System.out.println(emp2);
}
1.1 缓存失效情形二
/**
 * 缓存失效情形:
 * 不同sqlSession,缓存不同
 *
 * DEBUG 12-06 22:46:28,797 ==>  Preparing: select * from t_emp where id = ? (BaseJdbcLogger.java:135)
 * DEBUG 12-06 22:46:28,893 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:135)
 * DEBUG 12-06 22:46:29,058 <==      Total: 1 (BaseJdbcLogger.java:135)
 * Emp{id=1, userName='张三丰', passWord='123', sex='男', deptId=1}
 * DEBUG 12-06 22:46:29,219 ==>  Preparing: select * from t_emp where id = ? (BaseJdbcLogger.java:135)
 * DEBUG 12-06 22:46:29,221 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:135)
 * DEBUG 12-06 22:46:29,306 <==      Total: 1 (BaseJdbcLogger.java:135)
 * Emp{id=1, userName='张三丰', passWord='123', sex='男', deptId=1}
 */
@Test
public void testCache03(){
    SqlSession sqlSession = SqlSessionUtil.getSqlSession();
    CacheMapper cacheMapper = sqlSession.getMapper(CacheMapper.class);
    // 不同session第一次执行查询,走数据库
    Emp emp = cacheMapper.selectEmpById(1);
    System.out.println(emp);

    SqlSession sqlSession2 = SqlSessionUtil.getSqlSession();
    CacheMapper cacheMapper2 = sqlSession2.getMapper(CacheMapper.class);
    // 不同session第二次执行查询,走数据库
    Emp emp2 = cacheMapper2.selectEmpById(1);
    System.out.println(emp2);
}

2、SqlSessionFactory缓存

开启二级缓存(SqlSessionFactory缓存)的条件:

  • 在核心配置文件中,设置全局配置属性cacheEnabled=“true”,默认为true,不需要设置
  • 在映射文件中设置标签
  • 二级缓存必须在SqlSession关闭或提交之后有效
  • 查询的数据所转换的实体类类型必须实现序列化的接口

在mybatis-config.xml中开启缓存

<settings>
    <!-- 开启缓存 -->
    <setting name="cacheEnabled" value="true"/>
</settings>

实体类实现序列化:

public class Emp implements Serializable {
    private Integer id;
    private String userName;
    private String passWord;
    private String sex;
    private Integer deptId;
    // getter setter
}

在映射文件CacheMapper.xml中配置缓存:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.giser.cache.mapper.CacheMapper">

    <!-- 开启二级缓存 -->
    <cache />
</mapper>

二级缓存测试:

package com.giser.cache.mapper;

import com.giser.cache.pojo.Emp;
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 org.junit.Test;

import java.io.IOException;
import java.io.InputStream;

/**
 * @author giserDev
 * @description mybatis二级缓存SessionFactory级别
 * @date 2023-12-07 19:23:32
 */
public class SessionFactoryLevelCacheMapperTest {

    /**
     * 二级缓存是SqlSessionFactory级别,通过同一个SqlSessionFactory创建的SqlSession查询的结果会被
     * 缓存;此后若再次执行相同的查询语句,结果就会从缓存中获取
     *
     * 二级缓存开启的条件:
     * a>在核心配置文件mybatis-config.xml中,设置全局配置属性cacheEnabled="true",默认为true,不需要设置
     *         开启缓存
     *         <setting name="cacheEnabled" value="true"/>
     *
     * b>在映射文件CacheMapper.xml中设置标签
 *             <cache />
     * c>二级缓存必须在SqlSession关闭或提交之后有效
     *         sqlSession1.commit();
     *
     * d>查询的数据所转换的实体类类型必须实现序列化的接口
     *         public class Emp implements Serializable
     * 			开启二级缓存后,如果对应的实体类没有实现序列化,
     * 			则会报错:Cause: org.apache.ibatis.cache.CacheException: Error serializing object.  Cause: java.io.NotSerializableException: com.giser.cache.pojo.Emp
     *
     * 使二级缓存失效的情况:
     * 两次查询之间执行了任意的增删改,会使一级和二级缓存同时失效
     *
     */
    @Test
    public void testSessionFactoryCache(){
        try {
            InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
            SqlSession sqlSession1 = sqlSessionFactory.openSession();
            CacheMapper mapper1 = sqlSession1.getMapper(CacheMapper.class);
            Emp emp1 = mapper1.selectEmpById(1);
            System.out.println(emp1);

            // 二级缓存必须在SqlSession关闭或提交之后有效
            sqlSession1.commit();

            SqlSession sqlSession2 = sqlSessionFactory.openSession();
            CacheMapper mapper2 = sqlSession2.getMapper(CacheMapper.class);
            Emp emp2 = mapper2.selectEmpById(1);
            System.out.println(emp2);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

执行情况下:

DEBUG 12-07 19:53:57,604 Cache Hit Ratio [com.giser.cache.mapper.CacheMapper]: 0.0 (LoggingCache.java:60)
DEBUG 12-07 19:53:58,077 ==>  Preparing: select  from t_emp where id = ? (BaseJdbcLogger.java:135)
DEBUG 12-07 19:53:58,153 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:135)
DEBUG 12-07 19:53:58,239 <==      Total: 1 (BaseJdbcLogger.java:135)
Emp{id=1, userName='张三丰', passWord='123', sex='男', deptId=1}
WARN  12-07 19:53:58,385 As you are using functionality that deserializes object streams, it is recommended to define the JEP-290 serial filter. Please refer to https://docs.oracle.com/pls/topic/lookup?ctx=javase15&id=GUID-8296D8E8-2B93-4B9A-856E-0A65AF9B8C66 (SerialFilterChecker.java:45)
DEBUG 12-07 19:53:58,397 Cache Hit Ratio [com.giser.cache.mapper.CacheMapper]: 0.5 (LoggingCache.java:60)
Emp{id=1, userName='张三丰', passWord='123', sex='男', deptId=1}

可以看到缓存命中率指标参数(Cache Hit Ratio)由0.0变成了0.5,且第二次并没有查询数据库,说明缓存生效且命中了。

2.1 二级缓存相关配置

在mapper配置文件中添加的cache标签可以设置一些属性:

  • eviction属性:缓存回收策略
    • LRU(Least Recently Used) – 最近最少使用的:移除最长时间不被使用的对象。
    • FIFO(First in First out) – 先进先出:按对象进入缓存的顺序来移除它们。
    • SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。
    • WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。
      默认的是 LRU。
  • flushInterval属性:刷新间隔,单位毫秒
    默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新
  • size属性:引用数目,正整数
    代表缓存最多可以存储多少个对象,太大容易导致内存溢出
  • readOnly属性:只读,true/false
    • true:只读缓存;会给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改。这提供了
      很重要的性能优势。
    • false:读写缓存;会返回缓存对象的拷贝(通过序列化)。这会慢一些,但是安全,因此默认是
      false。

3、MyBatis缓存查询的顺序

先查询二级缓存,因为二级缓存中可能会有其他程序已经查出来的数据,可以拿来直接使用。
如果二级缓存没有命中,再查询一级缓存
如果一级缓存也没有命中,则查询数据库
SqlSession关闭之后,一级缓存中的数据会写入二级缓存

4、MyBatis整合第三方缓存EHCache

4.1 添加依赖
<!-- Mybatis EHCache整合包 -->
<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-ehcache</artifactId>
    <version>1.2.1</version>
</dependency>
<!-- slf4j日志门面的一个具体实现 -->
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>
4.2 EHCache配置文件ehcache.xml
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
         updateCheck="false">
    <!--
       diskStore:为缓存路径,ehcache分为内存和磁盘两级,此属性定义磁盘的缓存位置。参数解释如下:
       user.home – 用户主目录
       user.dir  – 用户当前工作目录
       java.io.tmpdir – 默认临时文件路径
     -->
    <diskStore path="user.dir"/>
    <!--
       defaultCache:默认缓存策略,当ehcache找不到定义的缓存时,则使用这个缓存策略。只能定义一个。
     -->
    <!--
      name:缓存名称。
      maxElementsInMemory:缓存最大数目
      maxElementsOnDisk:硬盘最大缓存个数。
      eternal:对象是否永久有效,一但设置了,timeout将不起作用。
      overflowToDisk:是否保存到磁盘,当系统宕机时
      timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。
      timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。
      diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.
      diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
      diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。
      memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。
      clearOnFlush:内存数量最大时是否清除。
      memoryStoreEvictionPolicy:可选策略有:LRU(最近最少使用,默认策略)、FIFO(先进先出)、LFU(最少访问次数)。
      FIFO,first in first out,这个是大家最熟的,先进先出。
      LFU, Less Frequently Used,就是上面例子中使用的策略,直白一点就是讲一直以来最少被使用的。如上面所讲,缓存的元素有一个hit属性,hit值最小的将会被清出缓存。
      LRU,Least Recently Used,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。
   -->

    <defaultCache
            maxElementsInMemory="1000"
            maxElementsOnDisk="10000000"
            eternal="false"
            overflowToDisk="true"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            diskExpiryThreadIntervalSeconds="120"
            memoryStoreEvictionPolicy="LRU" />

    <!--
    <cache
            name="cloud_user"
            eternal="false"
            maxElementsInMemory="5000"
            overflowToDisk="false"
            diskPersistent="false"
            timeToIdleSeconds="1800"
            timeToLiveSeconds="1800"
            memoryStoreEvictionPolicy="LRU"/>
    -->
</ehcache>

4.3 在Mapper.xml文件中配置二级缓存类型
    <!-- 开启二级缓存 -->
    <!-- 设置二级缓存类型 -->
    <cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
4.4 加入logback日志配置文件logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">
    <!-- 指定日志输出的位置 -->
    <appender name="STDOUT"
              class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
    <!-- 日志输出的格式 -->
    <!-- 按照顺序分别是:时间、日志级别、线程名称、打印日志的类、日志主体内容、换行 -->
    <pattern>[%d{HH:mm:ss.SSS}] [%-5level] [%thread] [%logger]
    [%msg]%n</pattern>
    </encoder>
    </appender>
    <!-- 设置全局日志级别。日志级别按顺序分别是:DEBUG、INFO、WARN、ERROR -->
    <!-- 指定任何一个日志级别都只打印当前级别和后面级别的日志。 -->
    <root level="DEBUG">
        <!-- 指定打印日志的appender,这里通过“STDOUT”引用了前面配置的appender -->
        <appender-ref ref="STDOUT" />
    </root>
    <!-- 根据特殊需求指定局部日志级别 -->
    <logger name="com.giser.cache.mapper" level="DEBUG"/>
</configuration>
```
此时再次运行相关测试代码,可以发现在用户当前工作目录下生成了缓存文件。

文章来源:https://blog.csdn.net/SUNBOYmxbsH/article/details/134912074
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。