MyBatis缓存机制详解
2025/9/17大约 8 分钟
MyBatis缓存机制详解
前置知识
在开始本教程之前,建议您已经完成:
- MyBatis基础入门教程
- MyBatis核心配置详解
- MyBatis增删改查操作
- MyBatis映射关系处理
- MyBatis动态SQL
- 了解缓存的基本概念
缓存概述
什么是缓存?
缓存是存在于内存中的临时数据,将用户经常查询的数据放在缓存(内存)中,用户去查询数据就不用从磁盘上(关系型数据库数据文件)查询,从缓存中查询,从而提高查询效率,解决了高并发系统的性能问题。
MyBatis缓存分类
MyBatis包含一个非常强大的查询缓存特性,它可以非常方便地配置和定制。MyBatis中默认定义了两级缓存:
- 一级缓存:SqlSession级别的缓存
- 二级缓存:SqlSessionFactory级别的缓存
一级缓存
一级缓存特点
一级缓存是SqlSession级别的,通过同一个SqlSession查询的数据会被缓存,下次查询相同的数据,就会从缓存中直接获取,不会从数据库重新访问。
一级缓存测试
@Test
public void testFirstLevelCache() {
SqlSession sqlSession = SqlSessionUtils.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
// 第一次查询
User user1 = mapper.getUserById(1);
System.out.println("第一次查询: " + user1);
// 第二次查询相同数据
User user2 = mapper.getUserById(1);
System.out.println("第二次查询: " + user2);
// 判断是否为同一个对象
System.out.println("是否为同一个对象: " + (user1 == user2));
sqlSession.close();
}
执行结果:
第一次查询: User{id=1, username='admin', ...}
第二次查询: User{id=1, username='admin', ...}
是否为同一个对象: true
一级缓存失效的四种情况
1. 不同的SqlSession对应不同的一级缓存
@Test
public void testDifferentSqlSession() {
// 第一个SqlSession
SqlSession sqlSession1 = SqlSessionUtils.getSqlSession();
UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
User user1 = mapper1.getUserById(1);
sqlSession1.close();
// 第二个SqlSession
SqlSession sqlSession2 = SqlSessionUtils.getSqlSession();
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
User user2 = mapper2.getUserById(1);
sqlSession2.close();
// 判断是否为同一个对象
System.out.println("是否为同一个对象: " + (user1 == user2)); // false
}
2. 同一个SqlSession但是查询条件不同
@Test
public void testDifferentCondition() {
SqlSession sqlSession = SqlSessionUtils.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
// 查询id=1的用户
User user1 = mapper.getUserById(1);
// 查询id=2的用户
User user2 = mapper.getUserById(2);
sqlSession.close();
}
3. 同一个SqlSession两次查询期间执行了任何一次增删改操作
@Test
public void testDMLOperation() {
SqlSession sqlSession = SqlSessionUtils.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
// 第一次查询
User user1 = mapper.getUserById(1);
// 执行增删改操作
mapper.updateUser(new User(1, "newName", "123", 25, "男", "test@test.com"));
// 第二次查询相同数据
User user2 = mapper.getUserById(1);
// 判断是否为同一个对象
System.out.println("是否为同一个对象: " + (user1 == user2)); // false
sqlSession.close();
}
4. 同一个SqlSession两次查询期间手动清空了缓存
@Test
public void testClearCache() {
SqlSession sqlSession = SqlSessionUtils.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
// 第一次查询
User user1 = mapper.getUserById(1);
// 手动清空缓存
sqlSession.clearCache();
// 第二次查询相同数据
User user2 = mapper.getUserById(1);
// 判断是否为同一个对象
System.out.println("是否为同一个对象: " + (user1 == user2)); // false
sqlSession.close();
}
二级缓存
二级缓存特点
二级缓存是SqlSessionFactory级别,通过同一个SqlSessionFactory创建的SqlSession查询的结果会被缓存;此后若再次执行相同的查询语句,结果就会从缓存中获取。
二级缓存开启条件
- 在核心配置文件中,设置全局配置属性
cacheEnabled="true"
,默认为true,不需要设置 - 在映射文件中设置标签
<cache />
- 二级缓存必须在SqlSession关闭或提交之后有效
- 查询的数据所转换的实体类类型必须实现序列化的接口
配置二级缓存
1. 实体类实现序列化接口
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private Integer id;
private String username;
private String password;
private Integer age;
private String sex;
private String email;
}
2. 在映射文件中配置缓存
<?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.example.mybatis.mapper.UserMapper">
<!-- 开启二级缓存 -->
<cache/>
<select id="getUserById" resultType="User">
select * from t_user where id = #{id}
</select>
</mapper>
二级缓存测试
@Test
public void testSecondLevelCache() {
// 第一个SqlSession
SqlSession sqlSession1 = SqlSessionUtils.getSqlSession();
UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
User user1 = mapper1.getUserById(1);
sqlSession1.close(); // 必须关闭SqlSession才能使用二级缓存
// 第二个SqlSession
SqlSession sqlSession2 = SqlSessionUtils.getSqlSession();
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
User user2 = mapper2.getUserById(1);
sqlSession2.close();
// 判断是否为同一个对象
System.out.println("是否为同一个对象: " + (user1 == user2)); // false,二级缓存返回的是副本
}
二级缓存相关配置
在mapper配置文件中添加的cache标签可以设置一些属性:
<cache
eviction="LRU"
flushInterval="60000"
size="512"
readOnly="true"/>
配置属性说明
属性 | 说明 | 默认值 |
---|---|---|
eviction | 缓存回收策略 | LRU |
flushInterval | 刷新间隔,单位毫秒 | 不设置 |
size | 引用数目,正整数 | 1024 |
readOnly | 只读,true/false | false |
缓存回收策略
- LRU(Least Recently Used):最近最少使用的,移除最长时间不被使用的对象
- FIFO(First in First out):先进先出,按对象进入缓存的顺序来移除它们
- SOFT:软引用,移除基于垃圾回收器状态和软引用规则的对象
- WEAK:弱引用,更积极地移除基于垃圾收集器状态和弱引用规则的对象
二级缓存失效情况
两次查询之间执行了任意的增删改,会使一级和二级缓存同时失效:
@Test
public void testSecondLevelCacheInvalidation() {
// 第一个SqlSession查询
SqlSession sqlSession1 = SqlSessionUtils.getSqlSession();
UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
User user1 = mapper1.getUserById(1);
sqlSession1.close();
// 第二个SqlSession执行增删改操作
SqlSession sqlSession2 = SqlSessionUtils.getSqlSession();
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
mapper2.updateUser(new User(1, "newName", "123", 25, "男", "test@test.com"));
sqlSession2.close();
// 第三个SqlSession查询相同数据
SqlSession sqlSession3 = SqlSessionUtils.getSqlSession();
UserMapper mapper3 = sqlSession3.getMapper(UserMapper.class);
User user3 = mapper3.getUserById(1); // 会重新查询数据库
sqlSession3.close();
}
MyBatis缓存查询顺序
- 先查询二级缓存,因为二级缓存中可能会有其他程序已经查出来的数据,可以拿来直接使用
- 如果二级缓存没有命中,再查询一级缓存
- 如果一级缓存也没有命中,则查询数据库
- SqlSession关闭之后,一级缓存中的数据会写入二级缓存
整合第三方缓存EHCache
添加依赖
<dependencies>
<!-- 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>
</dependencies>
创建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>
设置二级缓存的类型
在xxxMapper.xml文件中设置二级缓存类型:
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
加入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>
<!-- 设置全局日志级别 -->
<root level="DEBUG">
<appender-ref ref="STDOUT" />
</root>
<!-- 根据特殊需求指定局部日志级别 -->
<logger name="com.example.mybatis.mapper" level="DEBUG"/>
</configuration>
EHCache配置文件说明
属性名 | 是否必须 | 作用 |
---|---|---|
maxElementsInMemory | 是 | 在内存中缓存的element的最大数目 |
maxElementsOnDisk | 是 | 在磁盘上缓存的element的最大数目,若是0表示无穷大 |
eternal | 是 | 设定缓存的elements是否永远不过期 |
overflowToDisk | 是 | 设定当内存缓存溢出的时候是否将过期的element缓存到磁盘上 |
timeToIdleSeconds | 否 | 当缓存在EhCache中的数据前后两次访问的时间超过timeToIdleSeconds的属性取值时,这些数据便会删除 |
timeToLiveSeconds | 否 | 缓存element的有效生命期 |
diskSpoolBufferSizeMB | 否 | DiskStore(磁盘缓存)的缓存区大小 |
diskPersistent | 否 | 在VM重启的时候是否启用磁盘保存EhCache中的数据 |
diskExpiryThreadIntervalSeconds | 否 | 磁盘缓存的清理线程运行间隔 |
memoryStoreEvictionPolicy | 否 | 当内存缓存达到最大,有新的element加入的时候,移除缓存中element的策略 |
缓存最佳实践
1. 合理使用缓存
<!-- 对于频繁查询但不经常修改的数据,开启缓存 -->
<cache
eviction="LRU"
flushInterval="60000"
size="512"
readOnly="true"/>
<!-- 对于经常修改的数据,关闭缓存 -->
<select id="getUserById" resultType="User" useCache="false">
select * from t_user where id = #{id}
</select>
2. 缓存策略选择
<!-- 查询缓存 -->
<select id="getUserById" resultType="User" useCache="true">
select * from t_user where id = #{id}
</select>
<!-- 更新操作刷新缓存 -->
<update id="updateUser" parameterType="User" flushCache="true">
update t_user set username = #{username} where id = #{id}
</update>
3. 缓存监控
// 缓存统计信息
@Test
public void testCacheStatistics() {
SqlSession sqlSession = SqlSessionUtils.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
// 第一次查询
long startTime1 = System.currentTimeMillis();
User user1 = mapper.getUserById(1);
long endTime1 = System.currentTimeMillis();
System.out.println("第一次查询耗时: " + (endTime1 - startTime1) + "ms");
// 第二次查询(从缓存获取)
long startTime2 = System.currentTimeMillis();
User user2 = mapper.getUserById(1);
long endTime2 = System.currentTimeMillis();
System.out.println("第二次查询耗时: " + (endTime2 - startTime2) + "ms");
sqlSession.close();
}
总结
本教程详细介绍了MyBatis的缓存机制,包括:
- ✅ 缓存概述:缓存的基本概念和分类
- ✅ 一级缓存:SqlSession级别的缓存机制
- ✅ 二级缓存:SqlSessionFactory级别的缓存机制
- ✅ 缓存配置:各种缓存配置选项
- ✅ 第三方缓存:EHCache集成
- ✅ 最佳实践:缓存使用策略和性能优化
下一步学习
- 学习MyBatis的高级功能
- 了解逆向工程和分页插件
- 掌握QBC查询方式
在下一篇文章中,我们将学习MyBatis的高级功能,包括逆向工程、分页插件、QBC查询等实用工具。