二级缓存
导学
本节学习目标:
- 理解二级缓存的作用域与开启方式
- 掌握二级缓存与一级缓存的协作查询顺序
- 学会通过日志验证二级缓存的命中行为
- 明确二级缓存的数据一致性边界与刷新机制
定义
二级缓存(Second Level Cache)是MyBatis在Mapper命名空间级别维护的全局缓存,需要显式配置才能开启。它解决的核心性能痛点是:多个SqlSession执行相同查询时,避免每个会话都访问数据库,将热点数据提升到应用级内存中共享。
例如,乐途公司人事系统的员工信息查询接口,被多个并发请求调用,二级缓存可以让首次查询的结果被后续所有SqlSession复用。
核心原理
作用域与生命周期
| 特性 | 一级缓存 | 二级缓存 |
|---|---|---|
| 作用域 | SqlSession | Mapper Namespace |
| 默认状态 | 自动开启 | 需显式配置 |
| 共享范围 | 仅当前会话 | 跨SqlSession共享 |
| 生命周期 | 随SqlSession关闭而销毁 | 随应用启动而创建,随应用关闭而销毁 |
| 存储位置 | 内存(本地) | 可配置为内存、Ehcache、Redis等 |
查询顺序:三级穿透模型
MyBatis执行查询时,按照严格的优先级逐层查找:
关键协作规则:
- 查询时:先查二级缓存,未命中再查一级缓存,仍未命中则查数据库
- 写入时:查询结果先存入一级缓存
- 关闭时:SqlSession关闭(或commit)时,将一级缓存中的数据刷入二级缓存
- 清空时:执行insert/update/delete后,不仅清空当前SqlSession的一级缓存,还会通知二级缓存清空该命名空间下的相关缓存
开启条件
二级缓存需要同时满足两个条件:
- 全局开关:
mybatis-config.xml中settings.cacheEnabled = true(默认即为true) - 命名空间声明:Mapper XML中添加
<cache/>元素
完整示例
场景说明
乐途公司人事系统的高频查询场景:多个业务模块都需要查询员工基本信息。通过二级缓存,让第一个SqlSession的查询结果被第二个SqlSession直接命中,实现应用级共享。
操作前的数据库表结构及初始数据
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>
<!-- 开启二级缓存全局开关,默认true -->
<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"/>
</mappers>
</configuration>
StudentMapper.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.fly.mapper.StudentMapper">
<!-- 声明开启当前命名空间的二级缓存 -->
<cache/>
<!-- 实体类需实现Serializable,二级缓存会序列化存储 -->
<select id="selectById" resultType="com.fly.entity.Student">
SELECT id, name, age, major, score
FROM student
WHERE id = #{id}
</select>
<update id="updateScoreById" flushCache="true">
UPDATE student
SET score = #{score}
WHERE id = #{id}
</update>
</mapper>
Student.java
package com.fly.entity;
import java.io.Serializable;
public class Student implements Serializable {
private static final long serialVersionUID = 1L;
private Integer id;
private String name;
private Integer age;
private String major;
private Double score;
// Getter与Setter省略
}
SecondCacheDemo.java
package com.fly.demo;
import com.fly.entity.Student;
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 SecondCacheDemo {
public static void main(String[] args) throws Exception {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// ========== 第一个SqlSession ==========
System.out.println("=== 第一个SqlSession 第一次查询 id=2 ===");
try (SqlSession session1 = sqlSessionFactory.openSession()) {
StudentMapper mapper1 = session1.getMapper(StudentMapper.class);
Student s1 = mapper1.selectById(2);
System.out.println("结果:" + s1.getName() + ",专业:" + s1.getMajor());
// 必须commit或close,一级缓存才会刷入二级缓存
session1.commit();
}
System.out.println("\n=== 第一个SqlSession 已关闭 ===");
// ========== 第二个SqlSession ==========
System.out.println("\n=== 第二个SqlSession 查询 id=2 ===");
try (SqlSession session2 = sqlSessionFactory.openSession()) {
StudentMapper mapper2 = session2.getMapper(StudentMapper.class);
Student s2 = mapper2.selectById(2);
System.out.println("结果:" + s2.getName() + ",专业:" + s2.getMajor());
}
// ========== 第三个SqlSession 执行更新 ==========
System.out.println("\n=== 第三个SqlSession 更新 id=2 的分数 ===");
try (SqlSession session3 = sqlSessionFactory.openSession()) {
StudentMapper mapper3 = session3.getMapper(StudentMapper.class);
mapper3.updateScoreById(2, 91.0);
session3.commit(); // commit会清空二级缓存
}
// ========== 第四个SqlSession 再次查询 ==========
System.out.println("\n=== 第四个SqlSession 再次查询 id=2 ===");
try (SqlSession session4 = sqlSessionFactory.openSession()) {
StudentMapper mapper4 = session4.getMapper(StudentMapper.class);
Student s4 = mapper4.selectById(2);
System.out.println("结果:" + s4.getName() + ",分数:" + s4.getScore());
}
}
}
实际执行结果
控制台日志输出:
=== 第一个SqlSession 第一次查询 id=2 ===
[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: 2(Integer)
[com.fly.mapper.StudentMapper.selectById] - <== Total: 1
结果:白歌,专业:软件工程
=== 第一个SqlSession 已关闭 ===
=== 第二个SqlSession 查询 id=2 ===
[com.fly.mapper.StudentMapper.selectById] - Cache Hit Ratio [com.fly.mapper.StudentMapper]: 0.5
结果:白歌,专业:软件工程
=== 第三个SqlSession 更新 id=2 的分数 ===
[com.fly.mapper.StudentMapper.updateScoreById] - ==> Preparing: UPDATE student SET score = ? WHERE id = ?
[com.fly.mapper.StudentMapper.updateScoreById] - ==> Parameters: 91.0(Double), 2(Integer)
[com.fly.mapper.StudentMapper.updateScoreById] - <== Updates: 1
=== 第四个SqlSession 再次查询 id=2 ===
[com.fly.mapper.StudentMapper.selectById] - Cache Hit Ratio [com.fly.mapper.StudentMapper]: 0.3333333333333333
[com.fly.mapper.StudentMapper.selectById] - ==> Preparing: SELECT id, name, age, major, score FROM student WHERE id = ?
[com.fly.mapper.StudentMapper.selectById] - ==> Parameters: 2(Integer)
[com.fly.mapper.StudentMapper.selectById] - <== Total: 1
结果:白歌,分数:91.0
分析
- 第一次查询(session1):二级缓存命中率
0.0,未命中,发送SQL查库,查询白歌的信息。session1.commit()后,一级缓存数据刷入二级缓存 - 第二次查询(session2):二级缓存命中率
0.5,命中二级缓存!日志中无SQL输出,直接从二级缓存获取白歌的信息 - 执行更新(session3):
updateScoreById执行后commit,MyBatis自动清空该命名空间的二级缓存,保证数据一致性 - 第三次查询(session4):二级缓存命中率
0.333...,缓存已被清空,重新发送SQL查库,获取更新后的分数91.0
注意:
Cache Hit Ratio是MyBatis自动计算的二级缓存命中率统计,值越接近1说明缓存效果越好。
易错场景与常见误区
| 误区 | 正解 |
|---|---|
加了<cache/>就能跨SqlSession共享缓存 | 必须执行commit()或close(),一级缓存才会刷入二级缓存;只查询不提交,二级缓存不会生效 |
| 二级缓存返回的对象与一级缓存一样,是同一引用 | 二级缓存默认序列化存储,返回的是反序列化后的新对象,s1 == s2为false |
| 实体类不实现Serializable也能用二级缓存 | 若readOnly="false"(默认),实体类必须实现Serializable,否则抛出序列化异常 |
| 多个Mapper命名空间默认共享同一个缓存 | 每个<cache/>独立维护缓存,跨命名空间需用<cache-ref>显式引用 |
| 二级缓存比一级缓存更快 | 二级缓存涉及序列化/反序列化开销,同Session内一级缓存更快;二级缓存的优势在跨Session共享 |
反例:未commit导致二级缓存不生效
// 反例:查询后未commit,二级缓存不会生效
SqlSession session1 = sqlSessionFactory.openSession();
StudentMapper mapper1 = session1.getMapper(StudentMapper.class);
mapper1.selectById(3); // 查询小崔
// 没有调用 session1.commit() 或 session1.close()
session1.close(); // 如果close前没有commit,某些事务管理器下不会刷入二级缓存
SqlSession session2 = sqlSessionFactory.openSession();
StudentMapper mapper2 = session2.getMapper(StudentMapper.class);
mapper2.selectById(3); // 仍然发送SQL!因为一级缓存未刷入二级缓存
正确做法:查询后显式调用
session.commit(),确保一级缓存数据同步到二级缓存。
面试考点
Q1:MyBatis查询时的缓存查找顺序是什么?
先查二级缓存,未命中再查一级缓存,仍未命中则查询数据库。查询结果先写入一级缓存,SqlSession关闭或提交时,一级缓存数据刷入二级缓存。
Q2:二级缓存开启需要满足哪些条件?
两个条件:①
mybatis-config.xml中cacheEnabled=true(默认已开启);② 目标Mapper XML中声明<cache/>元素。此外,若readOnly="false",缓存实体类必须实现Serializable接口。
Q3:为什么SqlSession关闭后二级缓存才能被其他会话命中?
因为查询结果首先存入一级缓存,只有当前SqlSession执行
commit()或close()时,MyBatis才会将一级缓存中的数据序列化后刷入二级缓存。在此之前,数据仅对当前会话可见。
Q4:执行update后二级缓存会怎样?
MyBatis会清空该Mapper命名空间下的整个二级缓存(以及当前SqlSession的一级缓存),这是为了保证缓存与数据库的一致性。如果多个Mapper共享缓存(通过
<cache-ref>),被引用的缓存也会被清空。
小结
二级缓存将MyBatis的缓存能力从会话级提升到应用级,是解决热点数据重复查询的核心手段。其查询遵循"二级 → 一级 → 数据库"的三级穿透模型,数据回写则依赖SqlSession的提交或关闭。使用二级缓存时,务必注意实体类的序列化要求、commit时机的把控,以及缓存清空对数据一致性的保障。
下一章引子
二级缓存的默认实现基于内存的HashMap,其淘汰策略、刷新间隔、线程安全等都可以通过<cache>元素的属性精细控制。此外,实际生产环境往往需要将缓存外接到Ehcache、Redis等专业缓存中间件。下一节将系统讲解缓存配置详解,涵盖eviction策略、flushInterval、readOnly等核心属性,以及自定义缓存集成的完整方案。