缓存配置详解
导学
本节学习目标:
- 掌握
<cache>元素所有属性的含义与适用场景 - 理解四种eviction淘汰策略的差异与选择依据
- 学会通过
flushInterval、size等属性控制缓存行为 - 掌握
<cache-ref>、语句级useCache与flushCache的精细控制
定义
MyBatis二级缓存的默认实现是一个简单的内存HashMap,但生产环境对缓存的容量限制、淘汰策略、刷新周期、线程安全等有更高要求。<cache>元素及其相关配置机制解决的核心痛点是:在不编写代码的情况下,通过声明式配置精确控制缓存的存储策略、生命周期和并发行为,使缓存从"能用"走向"好用"。
核心原理
<cache>元素属性全解
<cache
eviction="LRU"
flushInterval="60000"
size="512"
readOnly="false"
blocking="false"/>
| 属性 | 默认值 | 说明 |
|---|---|---|
eviction | LRU | 缓存淘汰策略:LRU、FIFO、SOFT、WEAK |
flushInterval | 无 | 自动清空缓存的时间间隔(毫秒),默认不自动清空 |
size | 1024 | 缓存可保存的对象数量上限 |
readOnly | false | true返回同一实例(快但不安全),false返回序列化副本(线程安全) |
blocking | false | 是否启用阻塞缓存,防止缓存击穿 |
四种Eviction策略对比
| 策略 | 全称 | 淘汰机制 | 适用场景 |
|---|---|---|---|
| LRU | Least Recently Used | 最近最少使用 | 通用场景,默认策略,保留热点数据 |
| FIFO | First In First Out | 先进先出 | 数据时效性强的场景,按时间顺序淘汰 |
| SOFT | Soft Reference | 软引用 | 内存敏感场景,GC时优先回收,防OOM |
| WEAK | Weak Reference | 弱引用 | 比SOFT更激进,下次GC即回收,生命周期极短 |
缓存查询完整顺序流程
语句级缓存控制
MyBatis允许在单条SQL语句上覆盖全局缓存行为:
useCache="false":当前查询不使用二级缓存flushCache="true":当前语句执行后清空一级和二级缓存
跨命名空间缓存共享
<cache-ref namespace="com.fly.mapper.OtherMapper"/>可以让当前Mapper引用其他命名空间的缓存实例,实现多个Mapper共享同一缓存区域。执行任一关联Mapper的增删改,都会清空共享缓存。
完整示例
场景说明
乐途公司人事系统需要为不同业务模块配置差异化的缓存策略:
- 员工基础信息表:数据量大、访问频繁,使用LRU + 5分钟刷新
- 部门统计报表:数据时效性强,使用FIFO + 1分钟刷新
- 敏感薪资数据:禁用缓存,每次强制查库
操作前的数据库表结构及初始数据
CREATE TABLE student (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20),
age INT,
major VARCHAR(20),
score DECIMAL(5,2)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO student (name, age, major, score) VALUES
('大翔', 22, '计算机科学', 95.5),
('白歌', 21, '软件工程', 88.0),
('小崔', 20, '计算机科学', 92.0),
('黄俪', 21, '信息安全', 90.5),
('李眉', 22, '软件工程', 87.0);
当前数据状态:
| id | name | age | major | score |
|---|---|---|---|---|
| 1 | 大翔 | 22 | 计算机科学 | 95.50 |
| 2 | 白歌 | 21 | 软件工程 | 88.00 |
| 3 | 小崔 | 20 | 计算机科学 | 92.00 |
| 4 | 黄俪 | 21 | 信息安全 | 90.50 |
| 5 | 李眉 | 22 | 软件工程 | 87.00 |
完整代码与配置
mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<setting name="cacheEnabled" value="true"/>
<setting name="logImpl" value="SLF4J"/>
</settings>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/fly_db?useSSL=false&serverTimezone=UTC"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="com/fly/mapper/StudentMapper.xml"/>
<mapper resource="com/fly/mapper/ScoreMapper.xml"/>
</mappers>
</configuration>
StudentMapper.xml(LRU策略 + 5分钟刷新)
<?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.fly.mapper.StudentMapper">
<!-- LRU淘汰,最多缓存512条,每5分钟自动刷新,返回副本(线程安全) -->
<cache
eviction="LRU"
flushInterval="300000"
size="512"
readOnly="false"
blocking="true"/>
<select id="selectById" resultType="com.fly.entity.Student">
SELECT id, name, age, major, score
FROM student
WHERE id = #{id}
</select>
<!-- 查询全部学生,使用二级缓存 -->
<select id="selectAll" resultType="com.fly.entity.Student">
SELECT id, name, age, major, score FROM student
</select>
<update id="updateScoreById">
UPDATE student SET score = #{score} WHERE id = #{id}
</update>
</mapper>
ScoreMapper.xml(引用StudentMapper的缓存 + 禁用缓存查询)
<?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.fly.mapper.ScoreMapper">
<!-- 引用StudentMapper的缓存,实现共享 -->
<cache-ref namespace="com.fly.mapper.StudentMapper"/>
<!-- 敏感操作:查询时禁用二级缓存,强制走数据库 -->
<select id="selectSensitiveScore" resultType="com.fly.entity.Student" useCache="false">
SELECT id, name, score FROM student WHERE id = #{id}
</select>
<!-- 批量更新后强制清空所有缓存 -->
<update id="batchUpdateScore" flushCache="true">
UPDATE student SET score = score + #{bonus}
</update>
</mapper>
CacheConfigDemo.java
package com.fly.demo;
import com.fly.entity.Student;
import com.fly.mapper.ScoreMapper;
import com.fly.mapper.StudentMapper;
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 java.io.InputStream;
public class CacheConfigDemo {
public static void main(String[] args) throws Exception {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 场景1:验证LRU缓存与readOnly=false的行为
System.out.println("=== 场景1:LRU缓存 + readOnly=false ===");
SqlSession session1 = sqlSessionFactory.openSession();
StudentMapper mapper1 = session1.getMapper(StudentMapper.class);
Student s1 = mapper1.selectById(1); // 大翔
session1.commit();
session1.close();
SqlSession session2 = sqlSessionFactory.openSession();
StudentMapper mapper2 = session2.getMapper(StudentMapper.class);
Student s2 = mapper2.selectById(1); // 命中二级缓存
System.out.println("s1 == s2 ? " + (s1 == s2)); // false,因为是副本
System.out.println("s1.name = " + s1.getName() + ", s2.name = " + s2.getName());
session2.close();
// 场景2:验证useCache=false强制查库
System.out.println("\n=== 场景2:useCache=false 强制查库 ===");
SqlSession session3 = sqlSessionFactory.openSession();
ScoreMapper scoreMapper = session3.getMapper(ScoreMapper.class);
Student s3 = scoreMapper.selectSensitiveScore(1); // 强制发送SQL
System.out.println("敏感查询结果:" + s3.getName() + ",分数:" + s3.getScore());
session3.close();
// 场景3:验证flushCache=true清空缓存
System.out.println("\n=== 场景3:flushCache=true 清空缓存 ===");
SqlSession session4 = sqlSessionFactory.openSession();
StudentMapper mapper4 = session4.getMapper(StudentMapper.class);
Student s4 = mapper4.selectById(3); // 查询小崔,写入缓存
session4.commit();
session4.close();
SqlSession session5 = sqlSessionFactory.openSession();
ScoreMapper scoreMapper5 = session5.getMapper(ScoreMapper.class);
scoreMapper5.batchUpdateScore(5.0); // 清空共享缓存
session5.commit();
session5.close();
SqlSession session6 = sqlSessionFactory.openSession();
StudentMapper mapper6 = session6.getMapper(StudentMapper.class);
Student s6 = mapper6.selectById(3); // 缓存已被清空,重新查库
System.out.println("清空缓存后查询:" + s6.getName() + ",分数:" + s6.getScore());
session6.close();
}
}
实际执行结果
控制台日志输出:
=== 场景1:LRU缓存 + readOnly=false ===
[com.fly.mapper.StudentMapper.selectById] - Cache Hit Ratio [com.fly.mapper.StudentMapper]: 0.0
[com.fly.mapper.StudentMapper.selectById] - ==> Preparing: SELECT id, name, age, major, score FROM student WHERE id = ?
[com.fly.mapper.StudentMapper.selectById] - ==> Parameters: 1(Integer)
[com.fly.mapper.StudentMapper.selectById] - <== Total: 1
[com.fly.mapper.StudentMapper.selectById] - Cache Hit Ratio [com.fly.mapper.StudentMapper]: 0.5
s1 == s2 ? false
s1.name = 大翔, s2.name = 大翔
=== 场景2:useCache=false 强制查库 ===
[com.fly.mapper.ScoreMapper.selectSensitiveScore] - ==> Preparing: SELECT id, name, score FROM student WHERE id = ?
[com.fly.mapper.ScoreMapper.selectSensitiveScore] - ==> Parameters: 1(Integer)
[com.fly.mapper.ScoreMapper.selectSensitiveScore] - <== Total: 1
敏感查询结果:大翔,分数:95.5
=== 场景3:flushCache=true 清空缓存 ===
[com.fly.mapper.StudentMapper.selectById] - Cache Hit Ratio [com.fly.mapper.StudentMapper]: 0.0
[com.fly.mapper.StudentMapper.selectById] - ==> Preparing: SELECT id, name, age, major, score FROM student WHERE id = ?
[com.fly.mapper.StudentMapper.selectById] - ==> Parameters: 3(Integer)
[com.fly.mapper.StudentMapper.selectById] - <== Total: 1
[com.fly.mapper.ScoreMapper.batchUpdateScore] - ==> Preparing: UPDATE student SET score = score + ?
[com.fly.mapper.ScoreMapper.batchUpdateScore] - ==> Parameters: 5.0(Double)
[com.fly.mapper.ScoreMapper.batchUpdateScore] - <== Updates: 5
[com.fly.mapper.StudentMapper.selectById] - Cache Hit Ratio [com.fly.mapper.StudentMapper]: 0.0
[com.fly.mapper.StudentMapper.selectById] - ==> Preparing: SELECT id, name, age, major, score FROM student WHERE id = ?
[com.fly.mapper.StudentMapper.selectById] - ==> Parameters: 3(Integer)
[com.fly.mapper.StudentMapper.selectById] - <== Total: 1
清空缓存后查询:小崔,分数:97.0
分析
- readOnly=false的效果:
s1 == s2为false,说明二级缓存返回的是反序列化后的新对象,线程安全,但牺牲一定性能 - useCache=false的效果:
selectSensitiveScore每次都发送SQL,不命中任何缓存,适合敏感数据 - flushCache=true的效果:
batchUpdateScore执行后,由于ScoreMapper引用了StudentMapper的缓存(<cache-ref>),共享缓存被整体清空,后续查询重新发送SQL,且小崔的分数已更新为97.0(原92.0 + 5.0) - LRU策略:当缓存数量超过
size="512"时,最近最少被访问的条目会被淘汰
易错场景与常见误区
| 误区 | 正解 |
|---|---|
readOnly="true"返回的对象可以安全修改 | readOnly="true"返回同一实例,多线程修改会互相影响,仅适用于只读场景 |
flushInterval是缓存过期时间 | flushInterval是定时清空全部缓存的周期,不是单条数据的TTL;到期后整个缓存区被清空 |
size设置越大越好 | size过大占用内存,且二级缓存默认序列化存储,大size会增加GC压力;应根据实际数据量合理设置 |
blocking="true"会提升查询性能 | blocking是为了防止缓存击穿(多个线程同时查库),会牺牲一定并发性能换取数据库保护 |
<cache-ref>只是引用配置 | <cache-ref>引用的是缓存实例本身,增删改会清空被引用方的全部缓存,影响范围比预期大 |
反例:readOnly=true的线程安全问题
// 反例:readOnly="true"时修改缓存对象,影响其他线程
// Mapper配置:<cache readOnly="true"/>
SqlSession session1 = sqlSessionFactory.openSession();
StudentMapper mapper1 = session1.getMapper(StudentMapper.class);
Student s1 = mapper1.selectById(1); // 大翔
s1.setScore(0.0); // 直接修改缓存中的对象!
session1.commit();
session1.close();
SqlSession session2 = sqlSessionFactory.openSession();
StudentMapper mapper2 = session2.getMapper(StudentMapper.class);
Student s2 = mapper2.selectById(1); // 命中缓存,但分数已被篡改为0.0!
System.out.println(s2.getScore()); // 输出 0.0,数据已被污染
正解:除非能确保缓存对象绝对只读,否则使用
readOnly="false",让MyBatis返回序列化副本。
面试考点
Q1:MyBatis二级缓存的四种eviction策略有什么区别?
LRU(最近最少使用)淘汰最久未被访问的数据,适合保留热点;FIFO(先进先出)按进入顺序淘汰,适合时效性数据;SOFT(软引用)在内存不足时被GC回收,防OOM;WEAK(弱引用)更激进,下次GC即回收。生产环境默认LRU最通用。
Q2:readOnly="true"和readOnly="false"有什么区别?
true返回缓存中的同一对象实例,速度快但线程不安全,修改会影响所有读取者;false(默认)返回序列化后的副本,线程安全但需实现Serializable,有序列化开销。
Q3:flushInterval和flushCache有什么区别?
flushInterval是<cache>的属性,按固定周期自动清空整个命名空间的缓存;flushCache是语句级属性,执行该语句后立即清空缓存,是一次性行为。
Q4:多个Mapper如何共享同一个二级缓存?有什么风险?
通过
<cache-ref namespace="xxx"/>引用其他Mapper的缓存实例。风险在于:任一关联Mapper执行增删改,都会清空整个共享缓存区,可能导致缓存命中率骤降。建议只有强关联且数据一致性要求高的Mapper才共享缓存。
小结
<cache>元素是MyBatis二级缓存的"调度中心",通过eviction控制淘汰策略、flushInterval控制自动刷新周期、size控制容量上限、readOnly权衡安全与性能、blocking防御缓存击穿。配合语句级的useCache和flushCache,以及跨命名空间的<cache-ref>,开发者可以在不侵入业务代码的前提下,完成从粗放到精细的缓存治理。
下一章引子
MyBatis内置的二级缓存基于内存HashMap,无法满足分布式、大容量、持久化等企业级需求。下一节将讲解自定义缓存,通过实现Cache接口接入Ehcache、Redis等外部缓存中间件,让MyBatis缓存能力突破单机内存限制。