大家好,今天我们来聊MyBatis中面试高频、也是性能优化关键的缓存机制------一级缓存与二级缓存。
一、先搞懂:MyBatis缓存到底是什么?
MyBatis的缓存是为了减少数据库IO、提升查询性能而设计的,它的核心逻辑很简单:查询数据时,先去缓存里找,命中就直接返回;没命中才去查数据库,查到后把结果存入缓存,下次查询直接复用。
整体工作流程如下:
- 接收用户的查询请求
- 检查缓存中是否存在对应数据
- 命中缓存:直接返回缓存数据,无需访问数据库
- 未命中缓存:访问数据库查询数据,将结果存入缓存后再返回
MyBatis的缓存分为两级:一级缓存(SqlSession级) 和二级缓存(Mapper/Namespace级) ,两者的作用域、生命周期、默认配置完全不同,我们一个个来看。当开启二级缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库。
二、一级缓存:SqlSession级别的"本地缓存"
2.1 核心原理与默认配置
一级缓存是MyBatis默认开启的缓存机制,核心特点如下:
- 底层实现 :基于
PerpetualCache实现,本质就是一个HashMap,数据存在当前SqlSession的内存中 - 作用域 :仅在同一个SqlSession内共享,不同SqlSession的缓存互不影响
- 生命周期 :与SqlSession绑定,当SqlSession执行
flush()、close(),或执行增删改操作时,缓存会被清空 - 默认状态:无需任何配置,默认开启
2.2 代码示例:同一个Session内的缓存复用
我们用一段代码验证一级缓存的效果:
// 1. 获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
// 2. 获取UserMapper代理对象
UserMapper userMapper1 = sqlSession.getMapper(UserMapper.class);
UserMapper userMapper2 = sqlSession.getMapper(UserMapper.class);
// 第一次查询:未命中缓存,执行SQL查询数据库
User user = userMapper1.selectById(6);
System.out.println(user);
System.out.println("-------------------");
// 第二次查询:同一个SqlSession内,命中一级缓存,不执行SQL
User user1 = userMapper2.selectById(6);
System.out.println(user1);
sqlSession.close();
运行结果:控制台只会打印1次SQL语句日志,第二次查询直接复用了第一次的缓存数据,这就是一级缓存的作用。
2.3 一级缓存的常见失效场景
很多人以为一级缓存只有关闭Session才会失效,其实以下场景都会清空缓存:
- SqlSession执行了
flush()、close()操作 - SqlSession执行了增删改操作(
insert/update/delete),无论是否修改了当前查询的数据,都会清空该Session的一级缓存 - 手动调用
sqlSession.clearCache()方法 - 配置了
flushCache="true"的查询语句
三、二级缓存:Mapper/Namespace级别的"全局缓存"
3.1 核心原理与开启方式
二级缓存是跨SqlSession共享的缓存机制,核心特点如下:
- 底层实现 :同样基于
PerpetualCache+HashMap实现,但作用域是Mapper的Namespace,同一个Namespace下的所有SqlSession共享缓存 - 作用域:Mapper(Namespace)级别,不依赖SqlSession,不同SqlSession可以共享同一份缓存数据
- 默认状态:默认关闭,需要手动开启
- 同步:一级缓存数据并非只靠提交/关闭同步,刷新操作同样可以完成同步
开启二级缓存需要两步:
-
全局配置文件开启总开关 :在
<settings> <setting name="cacheEnabled" value="true"/> </settings>mybatis-config.xml的<settings>中配置 -
Mapper映射文件开启当前Namespace缓存 :在需要使用二级缓存的Mapper.xml中添加
<mapper namespace="com.example.mapper.UserMapper"> <cache/> </mapper><cache/>标签
3.2 代码示例:跨Session的缓存复用
我们用两个不同的SqlSession验证二级缓存的效果:
// 第一个SqlSession:查询数据并关闭(数据会同步到二级缓存)
SqlSession sqlSession1 = sqlSessionFactory.openSession();
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
User user1 = userMapper1.selectById(6);
System.out.println(user1);
sqlSession1.close(); // 关闭Session,数据同步到二级缓存
System.out.println("-------------------");
// 第二个SqlSession:查询数据,命中二级缓存,不执行SQL
SqlSession sqlSession2 = sqlSessionFactory.openSession();
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
User user2 = userMapper2.selectById(6);
System.out.println(user2);
sqlSession2.close();
运行结果:控制台只会打印1次SQL语句日志,第二个SqlSession直接复用了二级缓存中的数据,实现了跨Session的缓存共享。
3.3 二级缓存的关键注意事项
二级缓存的坑比一级缓存多,面试常考这几个点:
-
缓存更新机制 :当当前Namespace下执行增删改操作后,默认会清空该Namespace下的所有二级缓存(无论是否修改了缓存中的数据)
-
实体类必须序列化 :二级缓存存储的数据需要实现
Serializable接口,否则可能出现序列化异常(尤其是整合Redis等第三方缓存时)public class User implements Serializable {
private static final long serialVersionUID = 1L;
// 其他属性和方法
} -
脏读风险:二级缓存是Namespace隔离的,如果A表的修改操作不在当前Namespace中,当前Namespace的缓存不会被清空,就可能出现脏读(这也是很多企业禁用MyBatis自带二级缓存的原因)
3.4 高频重难点:Flush、Commit、Close 缓存同步规则
先看核心操作行为对照表,面试必背:
| 操作 | 同步一级缓存→二级缓存 | 提交数据库事务 | 清空一级缓存 | 关闭 SqlSession |
|---|---|---|---|---|
| sqlSession.commit() | ✅ 是 | ✅ 是 | ✅ 是 | ❌ 否 |
| sqlSession.close() | ✅ 是 | ✅ 自动提交 | ✅ 是 | ✅ 是 |
| sqlSession.flushStatements() | ✅ 是 | ❌ 否 | ✅ 是 | ❌ 否 |
核心结论
一级缓存的数据,在 SqlSession 提交(commit)、关闭(close)、刷新(flush)后,都会同步到二级缓存中;
未执行这三个操作前,二级缓存完全看不到一级缓存的新数据。
⚠️ 生产环境强制建议
-
优先使用
commit / close同步缓存,符合事务隔离规范,数据一致性有保障; -
非业务特殊场景,禁止手动调用 flushStatements ;
-
手动 flush 只会同步缓存、不会提交事务,极易出现「数据库事务回滚,但二级缓存已写入旧数据」的严重脏读问题。
四、面试高频:二级缓存什么时候会清理数据?
很多面试官会问:"MyBatis的二级缓存什么时候会清理数据?",核心规则只有一句话:
当某一个作用域(一级缓存Session/二级缓存Namespaces)执行了新增、修改、删除操作后,默认该作用域下所有select的缓存将被clear。
展开来说:
-
一级缓存:当前SqlSession执行增删改后,该Session的一级缓存被清空
-
二级缓存:当前Namespace下的任意增删改操作,都会清空该Namespace下的所有二级缓存
五、一级缓存 vs 二级缓存:核心区别对比
| 对比维度 | 一级缓存(SqlSession级) | 二级缓存(Mapper/Namespace级) |
|---|---|---|
| 作用域 | 单个SqlSession内 | 同一个Mapper/Namespace下 |
| 默认状态 | 开启 | 关闭(需手动配置) |
| 底层实现 | PerpetualCache + HashMap | PerpetualCache + HashMap |
| 数据共享范围 | 仅当前SqlSession可见 | 跨SqlSession共享 |
| 生命周期 | 随SqlSession关闭而清空 | 随应用/Namespace存活,手动清空 |
| 开启方式 | 无需配置,默认开启 | 两步配置:全局开关 + Mapper标签 |
| 失效触发条件 | 增删改、flush、close、clearCache | 当前Namespace增删改操作 |
六、实战避坑指南:这些误区一定要避开
- 误区1:以为二级缓存开启了就一定生效
很多同学只开了全局的cacheEnabled,忘了在Mapper.xml中加<cache/>标签,二级缓存根本不会生效。 - 误区2:实体类没实现Serializable接口
很多时候本地测试没问题,一旦整合第三方缓存(如Redis)就报错,就是因为实体类没有序列化。 - 误区3:以为跨Namespace的修改会清空缓存
二级缓存是Namespace隔离的,修改其他Namespace的数据不会清空当前Namespace的缓存,这会导致脏读,所以多表关联的复杂场景不建议用自带二级缓存。 - 误区4:不清楚 flush 会同步二级缓存
手动刷新会强制把一级缓存推入二级缓存,但不提交事务,是隐藏最深的脏读隐患。
七、总结:什么时候用?怎么用?
- 一级缓存:默认开启,无需额外配置,在同一个SqlSession内重复查询时会自动生效,能有效减少同事务内的重复查询,日常开发中不用特意关注,合理利用即可。
- 二级缓存 :适合查询多、修改少 的场景,比如字典表、配置表、基础数据字典,不适合频繁修改、跨Namespace关联的业务场景。
很多企业级项目会直接禁用MyBatis自带的二级缓存,改用Redis等分布式缓存,就是为了避免脏读和数据一致性问题。 - 缓存同步核心:commit / close / flush 都能完成一二缓存同步,生产环境严格规避手动 flush 操作,保证事务与缓存数据一致。
以上就是MyBatis一二级缓存的全部内容了,从原理到代码,再到面试考点、缓存同步规则和避坑指南,全覆盖讲解,面试遇到这类问题再也不用慌啦~