1 什么是 MyBatis 缓存

缓存就是将数据暂时存储在内存或硬盘中,当在查询数据时,如果缓存中有相同的数据就直接从缓存读取而不从数据库读取,从而减少 Java 应用与数据库的交互次数,这样就提升了程序的执行效率。比如查询 id = 1 的对象,第一次查询出对象之后会自动将该对象报存到缓存中,当下一次查询时,直接从缓存中去查找对象即可,无需再次访问数据库。

什么地方适用缓存:

  • 适用缓存:经常查询并且不经常改变的数据,数据的正确与否对最终结果影响不大。
  • 不适用缓存:经常改变的数据,数据的正确性与否对最终结果影响很大,比如:商品的库存,银行的存款,股市的牌价等等。

MyBatis 提供了两种缓存,它们分别为:一级缓存和二级缓存。

  • 一级缓存:指的是 SqlSession 对象级别的缓存。当我们执行查询后,查询的结果会同时存入到 SqlSession 为我们提供的一块区域中,该区域的结构是一个 HashMap,不同的 SqlSession 的缓存区域是互相不受影响的。当我们再次查询同样的数据,MyBatis 会先去 SqlSession 的缓存区域中查询是否有,有的话直接拿出来用,没有则去数据库查询。当 SqlSession 对象消失后(被 flush 或 close),MyBatis 的一级缓存也就消失了。(一级缓存默认是启动的,而且是一直存在的)
  • 二级缓存:指的是 Mapper 对象(Namspace)级别的缓存(也可以说是 SqlSessionFactory 对象级别的缓存,由同一个 SqlSessionFactory 对象创建的 SqlSession 共享其缓存)。多个 SqlSession 去操作同一个 Mapper 的 sql 语句,多个 SqlSession 可以共用二级缓存,二级缓存是跨 SqlSession 的。(二级缓存 MyBatis 默认是关闭的,需要自己去手动配置开启或可以自己选择用哪个厂家的缓存来作为二级缓存)

一级缓存和二级缓存的区别:

  • 相同点:它们都是基于 PerpetualCache 的 HashMap 本地缓存,
  • 不同点:一级缓存作用域为 SqlSession,而二级缓存作用域为 Mapper (Namespace),并且可自定义存储源,如 Ehcache,Redis,Memcache 等。

2 一级缓存

一级缓存是 SqlSession 对象级别的缓存,MyBatis 会在 SqlSession 内部维护一个 HashMap 用于存储,缓存 Key 为 hashcode+sqlid+sql,value 则为查询的结果集,当执行查询时会先从缓存区域查找,如果存在则直接返回数据,否则从数据库查询,并将结果集写入缓存区。

一级缓存示例(以 User 举例):

@Test
public void testSelectUserById(){
	//动态代理创建UserMapper对象
	UserMapper mapper = sqlSession.getMapper(UserMapper.class);
	//第一次查询
	User user1 = mapper.selectUserById(1);
	System.out.println(user1);
 
	System.out.println("--------------------------");
	//第二次查询
	User user2 = mapper.selectUserById(1);
	System.out.println(user2);
	sqlSession.close();
}

通过 log 可以看到,第二次查询并没有执行 SQL 语句,但是却获得了数据,说明是直接从缓存中读取的数据。

2.1 一级缓存清空时机

  1. 执行了 insert、update、delete 的 sql 语句。操作成功与否都会清空缓存,即使没有 commit 提交。
  2. 只执行了 commit 操作。sqlSession.commit(); 无论何时调用 commit 方法,就会清空缓存。
  3. 手动清空。sqlSession.clearCache();
  4. 同一个 SqlSession 但是查询条件不同

3 二级缓存

二级缓存是 Mapper 对象或 sqlSessionFactory 对象的缓存,由同一个 sqlSessionFactory 对象创建的 SqlSession 共享其缓存。当多个 SqlSession 去操作同一个 Mapper 的 sql 语句,多个 SqlSession 可以共用二级缓存,二级缓存是跨 SqlSession 的。(二级缓存 MyBatis 默认是关闭的,需要自己去手动配置开启或可以自己选择用哪个厂家的缓存来作为二级缓存)

3.1 配置文件启用二级缓存

第一步:在 MyBatis 的全局配置文件中加入如下配置。

<settings>
    <setting name="cacheEnabled" value="true"/>
</settings>

第二步:在 UserMapper.xml 配置文件中添加 cache 标签,让映射文件支持二级缓存。

<mapper namespace="...">
	<cache/>
	<resultMap id = "map" type="..">
		...

cache 标签还可以配置其它参数,如:

<cache eviction="LRU" flushInterval="60000"  size="512"  readOnly="true" type=”xxxxx” />

第三步:实体对象实现序列化接口,使用 Mybatis 二级缓存需要将 pojo 对象实现 java.io.serializable 接口,否则将出现序列化错误。

因为二级缓存有可能是存储在磁盘中,有文件的读写操作,所以映射的实体类要实现 Serializable 接口。

3.2 注解方式启用二级缓存

使用注解开启二级缓存

只需要在对应的 Mapper 接口上增加如下注解,就可以开启二级缓存,非常的方便。

@CacheNamespace(blocking = true)

3.3 二级缓存生效时机

sqlSession.close() 或者 sqlSession.commit() 时才生效。

3.4 禁用二级缓存

配置文件中useCache 属性是用来禁用二级缓存的,这个属性只有 <select> 标签有,它表示配置这个 select 是否使用二级缓存,默认为 true,设置为 false 则表示禁止当前 select 使用二级缓存

注解方式@Options(useCache=false)

3.5 刷新/清空二级缓存

查询时默认为 flushCache=false,因为查询时刷新缓存的话,就会导致一直去数据库查询,所以查询时必须要关闭;增删改默认为 flushCache=true。一般不更改,因为查询时不可能 true,增删改不可能 false。

即,两次查询之间执行了任意的增删改,会使一级和二级缓存同时失效。

配置文件中<delete id="" flushCache="true">

注解方式@Options(flushCache=Options.FlushCachePolicy.TRUE)

3.6 二级缓存其他配置属性

cache 标签属性介绍

eviction:

  • 缓存的回收策略,有四个策略,默认的是 LRU 。LRU — 最近最少使用,移除最长时间不被使用的对象;FIFO — 先进先出,按对象进入缓存的顺序来移除它们;SOFT — 软引用,移除基于垃圾回收器状态和软引用规则的对象;WEAK — 弱引用,更积极地移除基于垃圾收集器和弱引用规则的对象。

flushInterval:

  • 缓存刷新间隔,就是多久清空一次,默认不清空,单位毫秒,在执行配置了 flushCache 标签的 SQL 时清空。

size:

  • 缓存存放多少个元素,默认 1024。

readOnly:

  • 是否只读,默认为 false。false:读写,mybatis 觉得获取的数据可能会被修改,mybatis 会利用序列化和反序列化的技术克隆一份新的数据给你,这样虽然安全,但速度相对慢。true:只读,mybatis 认为所有从缓存中获取数据的操作都是只读操作,不会修改数据。mybatis 为了加快获取数据,直接就会将数据在缓存中的引用交给用户,这样不安全,但是速度快。

type:

  • 指定自定义缓存的全类名 (实现 Cache 接口即可)。

4 缓存查询策略

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

5 使用 redis 做二级缓存

要使用第三方的缓存,就必须实现 Mybatis 提供的 cache 接口,它自己有一个默认的实现类 PerpetualCache,源码如下:

public interface Cache {
  String getId();
  void putObject(Object key, Object value);
  Object getObject(Object key);
  Object removeObject(Object key);
  void clear();
  int getSize();
  ReadWriteLock getReadWriteLock();
}

添加 Maven 依赖

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.3.0</version>
</dependency>

开启二级缓存

<settings>
    <!--开启二级缓存-->
    <setting name="cacheEnabled" value="true"/>
    <!--日志-->
    <setting name="logImpl" value="STDOUT_LOGGING" />
</settings>

编写序列化和反序列化工具类

public class SerializableTools {
    public static Object byteArrayToObj(byte[] bt) throws Exception {
        ByteArrayInputStream bais = new ByteArrayInputStream(bt);
        ObjectInputStream ois = new ObjectInputStream(bais);
        return ois.readObject();
    }
 
    public static byte[] ObjToByteArray(Object obj) throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(obj);
        return bos.toByteArray();
    }
}
 

实现 Mybatis 二级缓存 org.apache.ibatis.cache.Cache 接口

public class RedisCache implements Cache {
    private Jedis jedis = new Jedis("127.0.0.1", 6379);
    /*
     *  MyBatis 会把映射文件的命名空间作为唯一标识 cacheId,
	 标识这个缓存策略属于哪个 namespace
	 这里定义好,并提供一个构造器,初始化这个 cacheId 即可
     */
    private String cacheId;
 
    public RedisCache (String cacheId){
        this.cacheId = cacheId;
    }
    //清空缓存
    @Override
    public void clear() {
        // 但这方法不建议实现
    }
    @Override
    public String getId() {
        return cacheId;
    }
    /**
     * MyBatis 会自动调用这个方法检测缓存中是否存在该对象。
	 * 既然是自己实现的缓存,那么当然是到 Redis 中找了。
     */
    @Override
    public Object getObject(Object arg0) { // arg0 在这里是键
        try {
            byte [] bt = jedis.get(SerializableTools.ObjToByteArray(arg0));
            if (bt == null) {        // 如果没有这个对象,直接返回 null
                return null;
            }
            return SerializableTools.byteArrayToObj(bt);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
    @Override
    public ReadWriteLock getReadWriteLock() {
        return new ReentrantReadWriteLock();
    }
    @Override
    public int getSize() {
        return Integer.parseInt(Long.toString(jedis.dbSize()));
    }
    /**
     * MyBatis 在读取数据时,会自动调用此方法将数据设置到缓存中。这里就写入 Redis
     */
    @Override
    public void putObject(Object arg0, Object arg1) {
        /*
         *  arg0 是 key , arg1 是值
         *  MyBatis 会把查询条件当做键,查询结果当做值。
         */
        try {
            jedis.set(SerializableTools.ObjToByteArray(arg0), SerializableTools.ObjToByteArray(arg1));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    /**
     * MyBatis 缓存策略会自动检测内存的大小,由此决定是否删除缓存中的某些数据
     */
    @Override
    public Object removeObject(Object arg0) {
        Object object = getObject(arg0);
        try {
            jedis.del(SerializableTools.ObjToByteArray(arg0));
        } catch (IOException e) {
            e.printStackTrace();
        }
        return object;
    }
}

指定自定义二级缓存

<cache type="com.thr.cache.RedisCache"/>

6 整合第三方缓存 EHCache(了解)

6.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>

6.2 各个 jar 包的功能

jar 包名称作用
mybatis-ehcacheMybatis 和 EHCache 的整合包
ehcacheEHCache 核心包
slf4j-apiSLF4J 日志门面包
logback-classic支持 SLF4J 门面接口的一个具体实现

6.3 创建 EHCache 的配置文件 ehcache.xml

  • 名字必须叫 ehcache.xml
<?xml version="1.0" encoding="utf-8" ?>
<ehcache xmlns:xsi=" http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">
    <!-- 磁盘保存路径 -->
    <diskStore path="D:\atguigu\ehcache"/>
    <defaultCache
            maxElementsInMemory="1000"
            maxElementsOnDisk="10000000"
            eternal="false"
            overflowToDisk="true"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            diskExpiryThreadIntervalSeconds="120"
            memoryStoreEvictionPolicy="LRU">
    </defaultCache>
</ehcache>

6.4 设置二级缓存的类型

  • 在 xxxMapper.xml 文件中设置二级缓存类型
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>

6.5 加入 logback 日志

  • 存在 SLF4J 时,作为简易日志的 log4j 将失效,此时我们需要借助 SLF4J 的具体实现 logback 来打印日志。创建 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.atguigu.crowd.mapper" level="DEBUG"/>
</configuration>

6.6 EHCache 配置文件说明

属性名是否必须作用
maxElementsInMemory在内存中缓存的 element 的最大数目
maxElementsOnDisk在磁盘上缓存的 element 的最大数目,若是 0 表示无穷大
eternal设定缓存的 elements 是否永远不过期。如果为 true,则缓存的数据始终有效,如果为 false 那么还要根据 timeToIdleSeconds、timeToLiveSeconds 判断
overflowToDisk设定当内存缓存溢出的时候是否将过期的 element 缓存到磁盘上
timeToIdleSeconds当缓存在 EhCache 中的数据前后两次访问的时间超过 timeToIdleSeconds 的属性取值时,这些数据便会删除,默认值是 0,也就是可闲置时间无穷大
timeToLiveSeconds缓存 element 的有效生命期,默认是 0.,也就是 element 存活时间无穷大
diskSpoolBufferSizeMBDiskStore(磁盘缓存)的缓存区大小。默认是 30MB。每个 Cache 都应该有自己的一个缓冲区
diskPersistent在 VM 重启的时候是否启用磁盘保存 EhCache 中的数据,默认是 false
diskExpiryThreadIntervalSeconds磁盘缓存的清理线程运行间隔,默认是 120 秒。每个 120s,相应的线程会进行一次 EhCache 中数据的清理工作
memoryStoreEvictionPolicy当内存缓存达到最大,有新的 element 加入的时候,移除缓存中 element 的策略。默认是 LRU(最近最少使用),可选的有 LFU(最不常使用)和 FIFO(先进先出