MyBatis 一级缓存与二级缓存原理详解

本文梳理 MyBatis 一级缓存、二级缓存的原理、区别与失效场景,并分析缓存选型策略。

目录


一、为什么需要缓存

每次查询都访问数据库会带来网络开销和 IO 开销。对于相同的查询条件,短时间内多次请求的结果往往是一样的,完全可以把第一次的结果缓存起来,后续直接返回,不再查库。

MyBatis 提供两级缓存:

请求进来
  ↓
查二级缓存(跨 Session 共享)→ 命中 → 直接返回
  ↓ 未命中
查一级缓存(当前 Session 内)→ 命中 → 直接返回
  ↓ 未命中
查数据库 → 返回结果 → 存入一级缓存
  ↓
Session 关闭 → 一级缓存数据转移到二级缓存

二、一级缓存

原理

一级缓存是 SqlSession 内部的一个 HashMap,key 由以下几部分组成:

namespace + statementId + 查询参数 + 分页参数

同一个 Session 内,相同参数的查询会命中缓存,直接返回,不再访问数据库。详细机制可参考 MyBatis 官方文档 — Local Cache

SqlSession session = factory.openSession();
StudentMapper mapper = session.getMapper(StudentMapper.class);

Student s1 = mapper.findById(1);  // 查数据库,存入缓存
Student s2 = mapper.findById(1);  // 命中缓存,直接返回

System.out.println(s1 == s2);  // true,是同一个对象

失效场景

场景 原因
执行了 INSERT / UPDATE / DELETE MyBatis 主动清空缓存,防止脏读
调用 session.clearCache() 手动清空
SqlSession 关闭 缓存随 Session 销毁
查询参数不同 key 不同,不会命中

作用域

一级缓存是 SqlSession 级别的,生命周期与 SqlSession 一致。原因是:

Mapper 代理对象内部持有 SqlSession 的引用,缓存存在 SqlSession 里,Session 关了缓存就没了。

实际价值

在 Web 项目中,每个 HTTP 请求都会新建一个 SqlSession,请求结束 Session 关闭。因此一级缓存只在同一次请求内的多次相同查询中生效,实际收益有限。


三、二级缓存

原理

二级缓存是 namespace 级别(即 Mapper 接口级别)的缓存,挂载在 SqlSessionFactory 下,生命周期与应用一致。

SqlSessionFactory(全局单例)
└── StudentMapper 二级缓存 { id=1→张三, id=2→李四, ... }
    ├── SqlSession1 关闭后,数据转移到这里
    ├── SqlSession2 查询时,先来这里找
    └── SqlSession3 查询时,先来这里找

多个 SqlSession 共享同一块二级缓存,这就是它能跨请求复用的根本原因。

为什么说是 factory 级别

一级缓存:缓存存在 SqlSession 里 → SqlSession 关了就没了 → SqlSession 级别
二级缓存:缓存挂在 factory 下   → factory 是单例活整个应用 → factory 级别

开启步骤

第一步: sqlMapConfig.xml 开启全局开关(默认已开启)

<settings>
    <setting name="cacheEnabled" value="true"/>
</settings>

第二步: 在 mapper.xml 中开启该 namespace 的缓存

<mapper namespace="com.demo.mapper.StudentMapper">
    <cache/>   <!-- 加这一行才生效 -->
    ...
</mapper>

注解开发则用 @CacheNamespace

@CacheNamespace
public interface StudentMapper { ... }

第三步: 实体类实现序列化接口

public class Student implements Serializable {
    // ...
}

为什么必须序列化

二级缓存可以将数据持久化到磁盘(不只是内存),序列化是将 Java 对象转换为字节流的必要条件。关于 Serializable 接口的详细说明可参考 Java 官方文档 — Serializable。不实现该接口会在运行时抛出异常。

为什么 user1 == user2 返回 false

User user1 = mapper1.findById(1);
session1.close();  // 关闭时,user1 序列化后存入二级缓存

User user2 = mapper2.findById(1);  // 从二级缓存取出,反序列化生成新对象
session2.close();

System.out.println(user1 == user2);      // false,两个不同的对象地址
System.out.println(user1.equals(user2)); // true,内容相同(需重写 equals)

原因: 二级缓存存储的是序列化后的字节数据,每次取出都会反序列化成一个新对象,内存地址不同,== 自然返回 false。


四、失效场景对比

失效原因 一级缓存 二级缓存
执行 CUD 操作 ✅ 失效 ✅ 失效(该 namespace)
Session 关闭 ✅ 失效 ❌ 不失效,数据转移过去
外部直接修改数据库 ✅ 下次 Session 自然感知 ❌ 不感知,可能脏读
集群其他实例修改 - ❌ 不感知,可能脏读
缓存到期(flushInterval) 无此机制 ✅ 强制过期

五、为什么要设置 flushInterval(刷新间隔)

MyBatis 的 CUD 操作只能清除本应用内的缓存,以下场景会产生脏读:

场景1:外部系统直接操作数据库

MyBatis 应用      其他系统/DBA
────────────      ──────────────
查用户,存缓存
                  直接 UPDATE user SET age=99
再次查用户
→ 缓存命中,返回旧数据  ← 脏读!

场景2:集群部署

实例1  缓存{ id=1 → 张三(age=18) }
实例2  缓存{ id=1 → 张三(age=18) }

实例1 执行 update age=99 → 清空实例1的缓存
实例2 完全不知道          → 缓存还是旧数据  ← 脏读!

flushInterval 的作用是设置一个强制过期时间,不管数据从哪里被修改,到期后缓存自动清空,下次查询强制回库:

<cache flushInterval="60000"/>  <!-- 每 60 秒强制刷新 -->

六、一级缓存 vs 二级缓存 总览

对比项 一级缓存 二级缓存
作用域 SqlSession 级别 namespace(Mapper)级别
存储位置 SqlSession 内的 HashMap SqlSessionFactory 下
默认状态 默认开启,无法关闭 需手动开启
生命周期 Session 关闭即销毁 应用运行期间持续存在
跨 Session ❌ 不支持 ✅ 支持
序列化要求 不需要 需要实现 Serializable
脏读风险 低(范围小) 较高(需配合 flushInterval)

七、实际项目中的缓存策略

MyBatis 的二级缓存在以下场景存在局限:

  • 集群部署:各实例缓存相互独立,数据不同步
  • 外部修改:无法感知非 MyBatis 渠道的数据变更
  • 高并发:缓存粒度粗,命中率有限

因此实际项目中,二级缓存往往被 Redis 替代。Redis 作为独立缓存中间件,天然支持集群共享,且过期策略更灵活,是生产环境的主流选择。可参考:


总结

知识点 结论
一级缓存级别 SqlSession 级别,Mapper 代理对象持有 Session 引用
二级缓存级别 factory 级别,factory 单例活整个应用
user1 == user2 为 false 反序列化生成新对象,地址不同
flushInterval 的意义 防范外部修改和集群场景的脏读,兜底保险
生产环境 二级缓存局限多,通常用 Redis 替代
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐