MyBatis 入门教程之六

缓存机制

MyBatis 内置了一个强大的事务性查询缓存机制,它可以非常方便地配置和定制。MyBatis 中默认定义了两级缓存:

  • 一级缓存和二级缓存
    • 默认情况下,只有一级缓存(SqlSession 级别的缓存,也称为本地缓存)开启
    • 二级缓存需要手动开启和配置,它是基于 namespace 级别的缓存
    • 为了提高扩展性,MyBatis 定义了缓存接口 Cache,可以通过实现 Cache 接口来自定义二级缓存

一级缓存

一级缓存介绍

  • 一级缓存(local cache),即本地缓存,作用域默认为 SqlSessionSession 执行 flushclose 操作后,该 Session 中的所有 Cache 将被清空。
  • 本地缓存默认是一直开启的(不能被关闭),但可以调用 sqlSession.clearCache() 来清空本地缓存,或者改变缓存的作用域。
  • 在 MyBatis 3.1 之后,支持配置本地缓存的作用域,在 mybatis.xml 中配置 localCacheScope 属性即可。MyBatis 利用本地缓存可以防止循环引用和加速重复嵌套查询。localCacheScope 属性的可选值为 SESSION | STATEMENT,默认值为 SESSION,这种情况下会缓存一个会话中执行的所有查询。若设置值为 STATEMENT,本地会话仅用在语句执行上,对相同 SqlSession 的不同调用将不会共享数据。
  • 同一个会话期间,只要查询过的数据都会被保存在当前 SqlSession 的一个 Map 中,它的 keyHashCode + 查询的 SqlId + 编写的 SQL 查询语句 + 参数 构成。

一级缓存使用案例

本节所需的案例代码,可以直接从 GitHub 下载对应章节 mybatis-lesson-16

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class MyBatisApplication {

public static void main(String[] args) throws IOException {

String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

SqlSession session = sqlSessionFactory.openSession();
try {
EmployeeMapper mapper = session.getMapper(EmployeeMapper.class);

// 第一次查询
Employee employee = mapper.getEmpById(1L);

// 第二次查询
Employee employee2 = mapper.getEmpById(1L);

// 判断两次查询到的对象的地址是否一致
System.out.println(employee == employee2);
} finally {
if (session != null) {
session.close();
}
}
}

}

上述代码执行之后,可以发现 MyBatis 只会发出一条 SQL 语句(如下所示),且两次查询到的对象的地址是相同的,即属于同一个对象,这是由于 MyBatis 一级缓存(默认开启)起的作用。

1
2
3
4
22:36:12,278 DEBUG getEmpById:137 - ==>  Preparing: select id, last_name as lastName, gender, email from t_employee where id = ?
22:36:12,317 DEBUG getEmpById:137 - ==> Parameters: 1(Long)
22:36:12,350 DEBUG getEmpById:137 - <== Total: 1
true

一级缓存失效的场景

一级缓存失效的四种场景:

  • 使用不同的 SqlSession 进行查询
  • 同一个 SqlSession,但是查询条件不同
  • 同一个 SqlSession,但是在两次查询期间手动清空了一级缓存
  • 同一个 SqlSession,但是在两次查询期间执行了任何一次增删改操作

二级缓存

二级缓存介绍

  • 二级缓存默认不开启,需要手动配置
  • 二级缓存在 SqlSession 提交或关闭之后才会生效
  • MyBatis 提供二级缓存的接口以及实现,缓存实现要求 POJO 实现 Serializable 接口
  • SqlSession 提交或者关闭后,一级缓存中的数据会被保存到二级缓存中,下次使用新的会话进行查询时,就可以使用到二级缓存的数据
  • 二级缓存(second level cache)是全局作用域缓存,并且是基于 namespace 级别的缓存,一个 namespace 对应一个二级缓存;不同 namespace 查询出的数据会放在自己对应的缓存(Map)中

二级缓存启用步骤

  • 第一步、在 MyBatis 的全局配置文件中开启二级缓存 <setting name= "cacheEnabled" value="true"/>
  • 第二步、在需要使用二级缓存的 SQL 映射文件里添加 <cache> 标签来配置缓存
  • 第三步、POJO(Plain Ordinary Java Object - 简单的 Java 对象)需要实现 Serializable 接口

二级缓存相关属性

在 MyBatis 的 SQL 映射文件中,<cache> 标签拥有以下属性:

二级缓存相关设置

  • 全局配置中的 cacheEnable 标签:启用二级缓存的开关,二级缓存默认是关闭的,一级缓存是一直是开启的
  • select 标签的 useCache 属性:配置当前的 select 语句是否使用二级缓存,一级缓存一直是使用的
  • SQL 标签的 flushCache 属性增删改操作默认是 flushCache=true,即 SQL 执行以后会同时清空一级和二级缓存,而查询操作默认是 flushCache=false
  • sqlSession.clearCache():只是用来清除一级缓存
  • 全局配置中的 localCacheScope 属性:本地缓存作用域(SESSION | STATEMENT),设置为 SESSION 时,当前会话的所有数据会被缓存到会话里;设置值为 STATEMENT 时,本地会话仅用在语句执行上,对相同 SqlSession 的不同调用将不会共享数据

值得一提的是,当在某一个作用域(一级缓存 / 二级缓存) 进行了增删改操作后,默认该作用域下的所有缓存数据将被清空。

二级缓存使用案例

本节所需的案例代码,可以直接从 GitHub 下载对应章节 mybatis-lesson-17

特别注意

  • 1、二级缓存在 SqlSession 提交或关闭之后才会生效
  • 2、当 SqlSession 提交或者关闭后,一级缓存中的数据会被保存到二级缓存中,下次使用新的会话进行查询时,就可以使用到二级缓存的数据
  • 全局配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
<configuration>

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

<!-- SQL映射文件 -->
<mappers>
<mapper resource="com/clay/mybatis/dao/EmployeeMapper.xml" />
</mappers>

</configuration>
  • SQL 映射文件
1
2
3
4
5
6
7
8
9
10
11
12
<mapper namespace="com.clay.mybatis.dao.EmployeeMapper">

<!-- 使用二级缓存 -->
<cache />

<select id="getEmpById" parameterType="Long" resultType="com.clay.mybatis.bean.Employee">
select id, last_name as lastName, gender, email
from t_employee
where id = #{id}
</select>

</mapper>
  • Java 代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class MyBatisApplication {

public static void main(String[] args) throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

SqlSession session = sqlSessionFactory.openSession();
SqlSession session2 = sqlSessionFactory.openSession();
try {
// 第一次查询
EmployeeMapper mapper = session.getMapper(EmployeeMapper.class);
Employee employee = mapper.getEmpById(1L);
// 会话提交
session.commit();

// 第二次查询
EmployeeMapper mapper2 = session2.getMapper(EmployeeMapper.class);
Employee employee2 = mapper2.getEmpById(1L);
// 会话提交
session2.commit();
} finally {
closeSesson(session);
closeSesson(session2);
}
}

public static void closeSesson(SqlSession session) {
if (session != null) {
session.close();
session = null;
}
}

}

执行上述的代码,可以发现 MyBatis 只会发出一条 SQL 语句(如下所示),这说明二级缓存起了作用。

1
2
3
22:55:46,112 DEBUG getEmpById:137 - ==>  Preparing: select id, last_name as lastName, gender, email from t_employee where id = ?
22:55:46,164 DEBUG getEmpById:137 - ==> Parameters: 1(Long)
22:55:46,203 DEBUG getEmpById:137 - <== Total: 1

一级与二级缓存原理图解

整合 Ehcache 第三方缓存

EhCache 是一个纯 Java 实现的进程内缓存框架,具有快速、精干等特点,是 Hibernate 中默认的 CacheProvider

Ehcache 第三方缓存整合步骤

  • 1、引入核心的依赖包,包括 Ehcache 包、MyBatis 整合包、日志包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>2.6.11</version>
</dependency>
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-ehcache</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.30</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.30</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
  • 2、在项目的 src/main/resources 目录下创建 ehcache.xml 配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">

<diskStore path="/tmp/ehcache" />

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

</ehcache>
标签说明
diskStore 指定缓存数据在磁盘中的存储位置
defaultCache 当借助 CacheManager.add("demoCache") 创建 Cache 时,EhCache 便会采用 <defalutCache> 标签指定的的缓存管理策略
属性必填说明
maxElementsInMemory在内存中缓存的 Element 的最大数量
maxElementsOnDisk在磁盘上缓存的 Element 的最大数量,若是 0 则表示无穷大
eternal设定缓存的 Elements 是否永远不过期。如果为 true,则缓存的数据始终有效,如果为 false ,则需要根据 timeToIdleSecondstimeToLiveSeconds 进行判断
overflowToDisk设定当内存缓存溢出的时候,是否将过期的 Element 缓存到磁盘上
timeToIdleSeconds当缓存在 EhCache 中的数据前后两次访问的时间超过 timeToIdleSeconds 的属性取值时,这些数据便会删除,默认值是 0,表示缓存数据的可闲置时间无穷大
timeToLiveSeconds缓存 Element 的有效生命期,默认是 0,表示 Element 的存活时间无穷大
diskSpoolBufferSizeMB这个参数设置 DiskStore(磁盘缓存) 的缓存区大小。默认是 30MB,每个 Cache 都应该有自己的一个缓冲区
diskPersistent在 VM 重启的时候是否启用磁盘保存 EhCache 中的数据,默认是 false
diskExpiryThreadIntervalSeconds磁盘缓存的清理线程运行间隔,默认是 120 秒。每隔 120 秒,相应的线程会进行一次 EhCache 中数据的清理工作
memoryStoreEvictionPolicy当内存缓存达到最大,有新的 Element 加入的时候,移除缓存中 Element 的策略。默认是 LRU(最近最少使用),可选值还有 LFU(最不常使用)FIFO(先进先出)
  • 3、配置 MyBatis 全局配置文件,开启二级缓存
1
2
3
4
5
6
7
8
<configuration>

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

</configuration>
  • 4、配置 SQL 映射文件,添加 <cache> 标签
1
2
3
4
5
6
<mapper namespace="com.clay.mybatis.dao.EmployeeMapper">

<!-- 指定 EhCache 作为二级缓存的实现 -->
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>

</mapper>

提示

  • 二级缓存是全局作用域缓存,并且是基于 namespace 级别的缓存,一个 namespace 对应一个二级缓存,不同 namespace 查询出的数据会放在自己对应的缓存(Map)中
  • 若想在命名空间(namespace)中共享相同的缓存配置和实例,则可以在 SQL 映射文件里使用 <cache-ref> 标签来引用另外一个缓存

假设在 DepartmentMapper.xml 映射文件中,想引用 com.clay.mybatis.dao.EmployeeMapper 命名空间的二级缓存,可以使用 <cache-ref> 标签来实现:

1
2
3
4
5
6
<mapper namespace="com.clay.mybatis.dao.DepartmentMapper">

<!-- 引用缓存,namespace:指定和哪个命名空间下的二级缓存一样 -->
<cache-ref namespace="com.clay.mybatis.dao.EmployeeMapper"/>

</mapper>
  • 5、验证整合结果

运行项目后,若输出的日志信息里有下面类似的内容,则说明 MyBatis 成功整合了 EhCache,或者可以查看 <diskStore> 标签指定的存储位置下有没有生成数据文件来验证整合结果。

1
2
3
23:35:03,108 DEBUG Segment:425 - put added 0 on heap
23:35:03,128 DEBUG Segment:779 - fault removed 0 from heap
23:35:03,129 DEBUG Segment:796 - fault added 0 on disk

使用第三方缓存的查询流程图

当执行一条查询 SQL 时,首先从二级缓存中进行查询,然后进入一级缓存中查询,最后执行 JDBC 查询。