说说 Mybatis 的缓存机制?
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于
Spring AI + Spring Boot3.x + JDK 21..., 点击查看; - 《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot3.x + JDK 17..., 点击查看项目介绍; 演示链接: http://116.62.199.48:7070/; - 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/
面试考察点
-
框架原理理解:面试官不仅仅是想知道你 "听说过" 一级缓存和二级缓存,更是想考察你是否理解 MyBatis 缓存的层级结构、作用范围、失效策略等底层机制。
-
生产踩坑意识:MyBatis 缓存在实际开发中容易踩坑(比如二级缓存脏读问题),面试官想知道你是否遇到过这些问题,以及如何解决。
-
架构设计思维:看你能否从缓存层次、作用域、生命周期等维度系统性地分析问题,而不是零散地背概念。
核心答案
MyBatis 提供了 两级缓存机制,用于减少数据库访问次数,提升查询性能:
| 维度 | 一级缓存 | 二级缓存 |
|---|---|---|
| 作用范围 | SqlSession 级别 | namespace(Mapper)级别 |
| 默认状态 | 默认开启,无法关闭 | 默认关闭,需手动开启 |
| 存储位置 | HashMap(内存) | 可自定义(内存 / Redis 等) |
| 生命周期 | 随着 SqlSession 关闭而销毁 | 随着 ApplicationContext 存活 |
| 跨 Session | 不可以 | 可以 |
| 失效条件 | 增删改操作、手动清空、关闭 | 增删改操作(仅影响同一 namespace) |
一句话结论:一级缓存是 SqlSession 级别的本地缓存,默认开启;二级缓存是 namespace 级别的全局缓存,需要手动开启。实际生产中,很多团队选择关闭二级缓存,改用 Redis 等外部缓存。
深度解析
一、缓存整体架构
MyBatis 的缓存查询遵循 "先查二级,再查一级,最后查数据库" 的顺序:
- 第一步:查询二级缓存。二级缓存是 namespace 级别的,所有
SqlSession共享。如果命中,直接返回结果。 - 第二步:查询一级缓存。二级缓存未命中时,进入当前
SqlSession的一级缓存。如果命中,直接返回结果,并且将结果写入二级缓存。 - 第三步:查询数据库。两级缓存都没命中,执行 SQL 查询数据库,结果写入一级缓存。当
SqlSession关闭(close())或提交(commit())时,一级缓存的数据才会写入二级缓存。
二、一级缓存详解
一级缓存基于 SqlSession,由 PerpetualCache 实现,底层就是一个简单的 HashMap。
核心特性:
- 同一个
SqlSession中,执行相同的 SQL 和参数,第二次直接从缓存取 - 默认开启,无法关闭
- 执行增删改操作后,缓存自动清空
- 调用
sqlSession.clearCache()手动清空
// 一级缓存示例
SqlSession session = sqlSessionFactory.openSession();
UserMapper mapper = session.getMapper(UserMapper.class);
// 第一次查询:查数据库,结果写入一级缓存
User user1 = mapper.selectById(1L);
// 第二次查询:相同的 SQL 和参数,直接命中一级缓存
User user2 = mapper.selectById(1L);
System.out.println(user1 == user2); // true,同一个对象引用!
// 执行增删改操作,一级缓存自动清空
mapper.updateById(new User(1L, "newName"));
// 第三次查询:缓存已清空,重新查数据库
User user3 = mapper.selectById(1L);
System.out.println(user1 == user3); // false,不同的对象
一级缓存失效的场景:
| 场景 | 说明 |
|---|---|
执行 INSERT / UPDATE / DELETE | 防止脏读,自动清空当前 SqlSession 的缓存 |
调用 sqlSession.clearCache() | 手动清空 |
SqlSession 关闭 | 缓存随 SqlSession 销毁 |
不同的 SqlSession | 一级缓存互相隔离,无法共享 |
三、二级缓存详解
二级缓存基于 namespace(即 Mapper 接口),多个 SqlSession 可以共享。
开启方式:
第一步,在 mybatis-config.xml 中全局开启(MyBatis 默认已开启总开关):
<!-- 全局二级缓存开关(默认 true,可省略) -->
<setting name="cacheEnabled" value="true"/>
第二步,在 Mapper XML 中添加 <cache/> 标签:
<!-- 开启当前 namespace 的二级缓存 -->
<cache
eviction="LRU" <!-- 淘汰策略:LRU 最近最少使用 -->
flushInterval="60000" <!-- 刷新间隔:60 秒 -->
size="1024" <!-- 缓存最大对象数 -->
readOnly="false" <!-- false 表示可读写(深拷贝),true 表示只读(浅拷贝) -->
/>
第三步,实体类实现 Serializable 接口:
// 二级缓存涉及序列化存储,实体类必须实现 Serializable
public class User implements Serializable {
private Long id;
private String username;
// ...
}
二级缓存的淘汰策略:
| 策略 | 说明 |
|---|---|
LRU(默认) | 最近最少使用,移除最长时间不被使用的对象 |
FIFO | 先进先出,按对象进入缓存的顺序移除 |
SOFT | 软引用,基于垃圾回收器状态和软引用规则移除 |
WEAK | 弱引用,更积极地基于垃圾回收器状态和弱引用规则移除 |
四、二级缓存的 "坑"
坑一:多表关联查询的脏读问题
这是二级缓存最大的坑。看下面这个场景:
UserMapper中有一个关联查询selectUserWithDept,结果包含了部门信息,被缓存到UserMapper的二级缓存中- 此时
DeptMapper更新了部门名称,DeptMapper的二级缓存被清空 - 但是
UserMapper的二级缓存并不知道部门信息变了,再次查询selectUserWithDept仍然返回旧的部门名称
解决方案:使用 Cache Ref 引用同一个缓存命名空间,或者干脆关闭二级缓存,改用 Redis 等外部缓存:
<!-- 让 UserMapper 引用 DeptMapper 的缓存空间(不推荐,耦合太强) -->
<cache-ref namespace="com.example.mapper.DeptMapper"/>
坑二:readOnly 的选择
readOnly = true:返回缓存对象的同一个引用,性能好但有被修改的风险readOnly = false:返回深拷贝对象,安全但有序列化开销
五、生产实践建议
| 建议 | 说明 |
|---|---|
| 一级缓存放心用 | 默认开启,Spring 集成下每次请求用新的 SqlSession,不会出问题 |
| 二级缓存慎用 | 多表关联场景容易脏读,建议关闭 |
| 优先用 Redis | 生产环境推荐用 Spring Cache + Redis 做业务缓存 |
| 分享型查询可开启 | 如果某个查询非常频繁且是单表查询,可以考虑开启该 namespace 的二级缓存 |
面试高频追问
-
追问一:
Spring整合 MyBatis 后,一级缓存还有效吗?- 有效,但作用范围缩小了。
Spring默认为每次请求创建新的SqlSession,所以一级缓存只在同一次请求、同一个方法调用链中生效。不同请求之间的一级缓存是隔离的。
- 有效,但作用范围缩小了。
-
追问二:如何禁用某个查询的缓存?
- 在
<select>标签上设置useCache="false"可以禁用二级缓存;设置flushCache="true"可以让查询同时清空一级缓存。
- 在
-
追问三:MyBatis 的缓存和
Spring Cache有什么区别?- MyBatis 缓存是框架内部的 SQL 级别缓存;
Spring Cache是业务层面的缓存抽象,可以对接 Redis、Caffeine 等,粒度更灵活,推荐生产环境使用。
- MyBatis 缓存是框架内部的 SQL 级别缓存;
常见面试变体
- 变体一:"MyBatis 一级缓存和二级缓存的区别?"
- 变体二:"MyBatis 二级缓存有什么问题?生产环境怎么用的?"
- 变体三:"MyBatis 缓存在什么情况下会失效?"
记忆口诀
一级 Session 级别默认开,二级 Namespace 级别手动来;查询先查二再查一,都没命中才查库;增删改操作清缓存,二级缓存怕多表关联。
总结
MyBatis 缓存分两级:一级缓存基于 SqlSession,默认开启,自动管理;二级缓存基于 namespace,需手动开启,跨 SqlSession 共享但存在多表关联脏读问题。生产环境建议谨慎使用二级缓存,复杂场景优先选择 Redis 等外部缓存方案。