说说 Mybatis 的缓存机制?
2026年01月06日
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 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 拥有一级缓存和二级缓存。
-
对两级缓存作用域和生命周期的理解:
-
一级缓存:是否理解它是
SqlSession级别的,以及其默认开启、自动失效的机制。 -
二级缓存:是否理解它是
Mapper/Namespace级别的,需要显式配置,以及其跨SqlSession共享的特性。
-
-
对缓存工作原理和源码机制的洞察:面试官不仅仅是想知道 “有哪两级缓存”,更是想知道缓存何时被创建、何时被命中、何时被清空,以及其底层的存储结构(如
PerpetualCache)。 -
对缓存使用场景和潜在问题的实践经验:能否结合高并发、事务等实际场景,讨论缓存的优势、劣势以及可能带来的数据一致性问题,并给出正确的最佳实践。
核心答案
MyBatis 提供了一个两级缓存机制,用于提升数据库查询性能:
- 一级缓存:默认开启,作用域为
SqlSession。在同一个SqlSession中,执行两次相同的 SQL 查询(参数相同),第二次会直接返回缓存的结果,不会再次访问数据库。 - 二级缓存:默认关闭,需要手动在 Mapper XML 文件中配置(
<cache/>)。其作用域为Mapper(Namespace),可以被多个SqlSession共享。当数据被提交(commit)或关闭(close)SqlSession后,一级缓存的数据才会被存入二级缓存。
深度解析
原理/机制
- 一级缓存:其底层是一个简单的
HashMap(PerpetualCache类)。SqlSession持有一个Executor,Executor持有一个Cache。当执行查询时,会生成一个唯一的CacheKey(由 SQL 语句、参数、分页信息等构成)。Executor首先查询缓存,若命中则直接返回,否则查询数据库并将结果存入缓存。- 缓存失效:当执行了
INSERT、UPDATE、DELETE操作,或调用了sqlSession.clearCache(),或对SqlSession执行了commit()/rollback(),该SqlSession的一级缓存会被全部清空。这是为了保证数据一致性。
- 缓存失效:当执行了
- 二级缓存:其作用域更广,存储结构更复杂。它也是基于
PerpetualCache,但外部包装了多个装饰器(ScheduledCache、LruCache等)来提供额外的功能,如 LRU 淘汰、定时刷新等。数据从一级缓存提交到二级缓存后,其他SqlSession在执行查询时,就可以从二级缓存中获取数据。
代码示例
1. 开启二级缓存 (Mapper XML):
<!-- 在 YourMapper.xml 中 -->
<mapper namespace="com.example.YourMapper">
<!-- 开启二级缓存并配置 -->
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>
...
</mapper>
2. 实体类需要实现 Serializable 接口:
public class User implements Serializable { // 必须序列化,因为二级缓存可能将数据写入磁盘或跨进程传输
private Long id;
private String name;
// getters and setters...
}
对比分析与最佳实践
| 特性 | 一级缓存 | 二级缓存 |
|---|---|---|
| 作用域 | SqlSession | Mapper(Namespace) |
| 默认状态 | 开启 | 关闭 |
| 存储结构 | 简单的 HashMap | HashMap + 装饰器(LRU, FIFO等) |
| 共享性 | 不可跨 SqlSession 共享 | 可跨 SqlSession 共享 |
| 失效时机 | 更新操作、clearCache、commit/rollback | 更新操作(同 namespace)、配置的刷新间隔 |
最佳实践:
- 谨慎使用二级缓存:在查询远多于修改、且数据对实时性要求不高的场景下使用。对于财务、交易等强一致性要求的场景,通常不建议开启。
- 注意事务隔离性:二级缓存可能导致“脏读”。例如,
SqlSession A查询了数据,SqlSession B更新了该数据但未提交,此时SqlSession A再次查询,从二级缓存中拿到的仍然是旧数据。 - 及时 commit:一级缓存的数据是在
SqlSession提交后才写入二级缓存的。如果你的操作模式是查询后不立即提交,那么其他SqlSession可能长时间无法看到新数据。
常见误区
- 误区一:“只要开启了二级缓存,性能就一定能提升。” —— 实际上,对于更新频繁的数据,缓存频繁失效会导致额外的维护开销,反而可能降低性能。
- 误区二:“一级缓存是
Statement级别的。” —— 这是一个经典误解。MyBatis 一级缓存是SqlSession级别的,并且默认开启。 - 误区三:“可以忽略
Serializable。” —— 如果实体类未实现Serializable,在开启二级缓存并尝试存储时,运行时可能会抛出序列化异常。
总结
MyBatis 的两级缓存机制(SqlSession 级别的一级缓存和 Mapper 级别的二级缓存)是其提升性能的关键特性,但理解其作用域、生命周期以及潜在的数据一致性问题,远比简单地开启它们更为重要。在实际项目中,应根据业务场景审慎评估是否使用及如何使用二级缓存。