说说 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 拥有一级缓存和二级缓存。

  • 对两级缓存作用域和生命周期的理解

    • 一级缓存:是否理解它是 SqlSession 级别的,以及其默认开启、自动失效的机制。

    • 二级缓存:是否理解它是 Mapper/Namespace 级别的,需要显式配置,以及其跨 SqlSession 共享的特性。

  • 对缓存工作原理和源码机制的洞察:面试官不仅仅是想知道 “有哪两级缓存”,更是想知道缓存何时被创建、何时被命中、何时被清空,以及其底层的存储结构(如 PerpetualCache)。

  • 对缓存使用场景和潜在问题的实践经验:能否结合高并发、事务等实际场景,讨论缓存的优势、劣势以及可能带来的数据一致性问题,并给出正确的最佳实践。

核心答案

MyBatis 提供了一个两级缓存机制,用于提升数据库查询性能:

  1. 一级缓存:默认开启,作用域为 SqlSession。在同一个 SqlSession 中,执行两次相同的 SQL 查询(参数相同),第二次会直接返回缓存的结果,不会再次访问数据库。
  2. 二级缓存:默认关闭,需要手动在 Mapper XML 文件中配置(<cache/>)。其作用域为 MapperNamespace),可以被多个 SqlSession 共享。当数据被提交(commit)或关闭(closeSqlSession 后,一级缓存的数据才会被存入二级缓存。

深度解析

原理/机制

  • 一级缓存:其底层是一个简单的 HashMapPerpetualCache 类)。SqlSession 持有一个 ExecutorExecutor 持有一个 Cache。当执行查询时,会生成一个唯一的 CacheKey(由 SQL 语句、参数、分页信息等构成)。Executor 首先查询缓存,若命中则直接返回,否则查询数据库并将结果存入缓存。
    • 缓存失效:当执行了 INSERTUPDATEDELETE 操作,或调用了 sqlSession.clearCache(),或对 SqlSession 执行了 commit()/rollback(),该 SqlSession 的一级缓存会被全部清空。这是为了保证数据一致性。
  • 二级缓存:其作用域更广,存储结构更复杂。它也是基于 PerpetualCache,但外部包装了多个装饰器(ScheduledCacheLruCache 等)来提供额外的功能,如 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...
}

对比分析与最佳实践

特性一级缓存二级缓存
作用域SqlSessionMapperNamespace
默认状态开启关闭
存储结构简单的 HashMapHashMap + 装饰器(LRU, FIFO等)
共享性不可跨 SqlSession 共享可跨 SqlSession 共享
失效时机更新操作、clearCachecommit/rollback更新操作(同 namespace)、配置的刷新间隔

最佳实践

  1. 谨慎使用二级缓存:在查询远多于修改、且数据对实时性要求不高的场景下使用。对于财务、交易等强一致性要求的场景,通常不建议开启。
  2. 注意事务隔离性:二级缓存可能导致“脏读”。例如,SqlSession A 查询了数据,SqlSession B 更新了该数据但未提交,此时 SqlSession A 再次查询,从二级缓存中拿到的仍然是旧数据。
  3. 及时 commit:一级缓存的数据是在 SqlSession 提交后才写入二级缓存的。如果你的操作模式是查询后不立即提交,那么其他 SqlSession 可能长时间无法看到新数据。

常见误区

  • 误区一:“只要开启了二级缓存,性能就一定能提升。” —— 实际上,对于更新频繁的数据,缓存频繁失效会导致额外的维护开销,反而可能降低性能。
  • 误区二:“一级缓存是 Statement 级别的。” —— 这是一个经典误解。MyBatis 一级缓存是 SqlSession 级别的,并且默认开启。
  • 误区三:“可以忽略 Serializable。” —— 如果实体类未实现 Serializable,在开启二级缓存并尝试存储时,运行时可能会抛出序列化异常。

总结

MyBatis 的两级缓存机制(SqlSession 级别的一级缓存和 Mapper 级别的二级缓存)是其提升性能的关键特性,但理解其作用域、生命周期以及潜在的数据一致性问题,远比简单地开启它们更为重要。在实际项目中,应根据业务场景审慎评估是否使用及如何使用二级缓存。