MyBatis一级缓存与二级缓存深度解析

大家好,今天我们来聊MyBatis中面试高频、也是性能优化关键的缓存机制------一级缓存与二级缓存


一、先搞懂:MyBatis缓存到底是什么?

MyBatis的缓存是为了减少数据库IO、提升查询性能而设计的,它的核心逻辑很简单:查询数据时,先去缓存里找,命中就直接返回;没命中才去查数据库,查到后把结果存入缓存,下次查询直接复用

整体工作流程如下:

  1. 接收用户的查询请求
  2. 检查缓存中是否存在对应数据
  3. 命中缓存:直接返回缓存数据,无需访问数据库
  4. 未命中缓存:访问数据库查询数据,将结果存入缓存后再返回

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才会失效,其实以下场景都会清空缓存:

  1. SqlSession执行了flush()close()操作
  2. SqlSession执行了增删改操作(insert/update/delete),无论是否修改了当前查询的数据,都会清空该Session的一级缓存
  3. 手动调用sqlSession.clearCache()方法
  4. 配置了flushCache="true"的查询语句

三、二级缓存:Mapper/Namespace级别的"全局缓存"

3.1 核心原理与开启方式

二级缓存是跨SqlSession共享的缓存机制,核心特点如下:

  • 底层实现 :同样基于PerpetualCache+HashMap实现,但作用域是Mapper的Namespace,同一个Namespace下的所有SqlSession共享缓存
  • 作用域:Mapper(Namespace)级别,不依赖SqlSession,不同SqlSession可以共享同一份缓存数据
  • 默认状态:默认关闭,需要手动开启
  • 同步:一级缓存数据并非只靠提交/关闭同步,刷新操作同样可以完成同步

开启二级缓存需要两步:

  1. 全局配置文件开启总开关 :在mybatis-config.xml<settings>中配置

    <settings> <setting name="cacheEnabled" value="true"/> </settings>
  2. Mapper映射文件开启当前Namespace缓存 :在需要使用二级缓存的Mapper.xml中添加<cache/>标签

    <mapper namespace="com.example.mapper.UserMapper"> <cache/> </mapper>

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 二级缓存的关键注意事项

二级缓存的坑比一级缓存多,面试常考这几个点:

  1. 缓存更新机制 :当当前Namespace下执行增删改操作后,默认会清空该Namespace下的所有二级缓存(无论是否修改了缓存中的数据)

  2. 实体类必须序列化 :二级缓存存储的数据需要实现Serializable接口,否则可能出现序列化异常(尤其是整合Redis等第三方缓存时)

    public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    // 其他属性和方法
    }

  3. 脏读风险:二级缓存是Namespace隔离的,如果A表的修改操作不在当前Namespace中,当前Namespace的缓存不会被清空,就可能出现脏读(这也是很多企业禁用MyBatis自带二级缓存的原因)

3.4 高频重难点:Flush、Commit、Close 缓存同步规则

先看核心操作行为对照表,面试必背:

操作 同步一级缓存→二级缓存 提交数据库事务 清空一级缓存 关闭 SqlSession
sqlSession.commit() ✅ 是 ✅ 是 ✅ 是 ❌ 否
sqlSession.close() ✅ 是 ✅ 自动提交 ✅ 是 ✅ 是
sqlSession.flushStatements() ✅ 是 ❌ 否 ✅ 是 ❌ 否

核心结论

一级缓存的数据,在 SqlSession 提交(commit)、关闭(close)、刷新(flush)后,都会同步到二级缓存中;

未执行这三个操作前,二级缓存完全看不到一级缓存的新数据。

⚠️ 生产环境强制建议

  1. 优先使用 commit / close 同步缓存,符合事务隔离规范,数据一致性有保障;

  2. 非业务特殊场景,禁止手动调用 flushStatements

  3. 手动 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. 误区1:以为二级缓存开启了就一定生效
    很多同学只开了全局的cacheEnabled,忘了在Mapper.xml中加<cache/>标签,二级缓存根本不会生效。
  2. 误区2:实体类没实现Serializable接口
    很多时候本地测试没问题,一旦整合第三方缓存(如Redis)就报错,就是因为实体类没有序列化。
  3. 误区3:以为跨Namespace的修改会清空缓存
    二级缓存是Namespace隔离的,修改其他Namespace的数据不会清空当前Namespace的缓存,这会导致脏读,所以多表关联的复杂场景不建议用自带二级缓存。
  4. 误区4:不清楚 flush 会同步二级缓存
    手动刷新会强制把一级缓存推入二级缓存,但不提交事务,是隐藏最深的脏读隐患。

七、总结:什么时候用?怎么用?

  1. 一级缓存:默认开启,无需额外配置,在同一个SqlSession内重复查询时会自动生效,能有效减少同事务内的重复查询,日常开发中不用特意关注,合理利用即可。
  2. 二级缓存 :适合查询多、修改少 的场景,比如字典表、配置表、基础数据字典,不适合频繁修改、跨Namespace关联的业务场景。
    很多企业级项目会直接禁用MyBatis自带的二级缓存,改用Redis等分布式缓存,就是为了避免脏读和数据一致性问题。
  3. 缓存同步核心:commit / close / flush 都能完成一二缓存同步,生产环境严格规避手动 flush 操作,保证事务与缓存数据一致。

以上就是MyBatis一二级缓存的全部内容了,从原理到代码,再到面试考点、缓存同步规则和避坑指南,全覆盖讲解,面试遇到这类问题再也不用慌啦~

相关推荐
身如柳絮随风扬2 小时前
MyBatis 与 Spring 中的设计模式
spring·设计模式·mybatis
范什么特西6 小时前
第一个Mybatis
java·开发语言·mybatis
Java成神之路-10 小时前
MyBatis 关联查询的延迟加载与积极加载原理
java·mybatis
Don.TIk11 小时前
天机の学堂
java·spring boot·spring·maven·mybatis
Devin~Y11 小时前
大厂Java面试实录:Spring Boot/JPA/Redis/Kafka/K8s 可观测性 + Spring AI RAG/Agent(小Y翻车现场)
java·spring boot·redis·mybatis·hibernate·spring mvc·jpa
小碗羊肉11 小时前
【JavaWeb | 第六篇】Mybatis
mybatis
拙野1 天前
工作中Mybatis动态SQL的使用
java·sql·mybatis
布局呆星1 天前
Spring Boot+MyBatis-Plus+Vue3前后端协作Note
spring boot·typescript·vue·mybatis
空中海1 天前
MyBatis 知识框架图、性能优化与面试题
性能优化·mybatis