说说 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/

面试考察点

  1. 框架原理理解:面试官不仅仅是想知道你 "听说过" 一级缓存和二级缓存,更是想考察你是否理解 MyBatis 缓存的层级结构、作用范围、失效策略等底层机制。

  2. 生产踩坑意识:MyBatis 缓存在实际开发中容易踩坑(比如二级缓存脏读问题),面试官想知道你是否遇到过这些问题,以及如何解决。

  3. 架构设计思维:看你能否从缓存层次、作用域、生命周期等维度系统性地分析问题,而不是零散地背概念。

核心答案

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 的二级缓存

面试高频追问

  1. 追问一Spring 整合 MyBatis 后,一级缓存还有效吗?

    • 有效,但作用范围缩小了。Spring 默认为每次请求创建新的 SqlSession,所以一级缓存只在同一次请求、同一个方法调用链中生效。不同请求之间的一级缓存是隔离的。
  2. 追问二:如何禁用某个查询的缓存?

    • <select> 标签上设置 useCache="false" 可以禁用二级缓存;设置 flushCache="true" 可以让查询同时清空一级缓存。
  3. 追问三:MyBatis 的缓存和 Spring Cache 有什么区别?

    • MyBatis 缓存是框架内部的 SQL 级别缓存;Spring Cache 是业务层面的缓存抽象,可以对接 Redis、Caffeine 等,粒度更灵活,推荐生产环境使用。

常见面试变体

  • 变体一:"MyBatis 一级缓存和二级缓存的区别?"
  • 变体二:"MyBatis 二级缓存有什么问题?生产环境怎么用的?"
  • 变体三:"MyBatis 缓存在什么情况下会失效?"

记忆口诀

一级 Session 级别默认开,二级 Namespace 级别手动来;查询先查二再查一,都没命中才查库;增删改操作清缓存,二级缓存怕多表关联。

总结

MyBatis 缓存分两级:一级缓存基于 SqlSession,默认开启,自动管理;二级缓存基于 namespace,需手动开启,跨 SqlSession 共享但存在多表关联脏读问题。生产环境建议谨慎使用二级缓存,复杂场景优先选择 Redis 等外部缓存方案。