MyBatis 的缓存机制主要分为两级(官方原生支持的),很多人在实际项目中还会自己扩展第三级缓存(比如接 Redis / Caffeine 等)。今天我们按最经典的思路来梳理一下,尽量讲得清楚一点。
一级缓存(PerpetualCache + SqlSession 级别)
- 范围:同一个 SqlSession 内部
- 默认:一直开启,无法通过配置完全关闭(只能调级别)
- 存储介质:默认使用
PerpetualCache(就是一个 HashMap) - Key 的组成:
statementId + 参数 + 分页参数(RowBounds) + SQL 语句本身(带 ? 的占位符形式) - 命中条件:完全相同的 SQL + 完全相同的参数 + 同一个 SqlSession
典型使用场景举例(最容易观察到一级缓存的地方):
try (SqlSession session = factory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
User u1 = mapper.selectById(1); // 查数据库
User u2 = mapper.selectById(1); // 直接命中一级缓存
User u3 = mapper.selectById(2); // 再查一次数据库
// 同一个 session 内,哪怕换个方式调用
User u4 = session.selectOne("com.xxx.UserMapper.selectById", 1); // 还是命中一级缓存
}
一级缓存什么时候失效 / 清空?(非常重要,面试高频)
- SqlSession 调用了 commit() / rollback() / close()
- 执行了本 namespace 下的 任何 update/insert/delete(即使不是同一条语句)
- 手动调用
session.clearCache() - 设置了
flushCache=true的查询语句 - 查询时用了
ResultHandler(自定义结果处理器)或Cursor(流式查询)
配置方式(一般不用改,但可以了解):
<setting name="localCacheScope" value="SESSION"/> <!-- 默认 SESSION -->
<!-- 或者改成 STATEMENT(相当于关闭了跨语句的一级缓存) -->
<setting name="localCacheScope" value="STATEMENT"/>
二级缓存(跨 SqlSession,namespace 级别)
- 范围:同一个 namespace(通常就是一个 Mapper.xml 或 @Mapper 接口)
- 默认:关闭的,需要手动开启
- 存储介质:默认
PerpetualCache(HashMap),但强烈建议换成支持序列化的实现(如 Ehcache、Redis、Caffeine 等) - 序列化要求:实体类必须实现 Serializable 接口(否则反序列化会报错)
开启二级缓存的完整步骤(最常见写法)
- 全局开启(mybatis-config.xml)
<settings>
<setting name="cacheEnabled" value="true"/> <!-- 默认就是 true,但建议写上 -->
</settings>
- 在具体的 Mapper.xml 上开启(最关键一步)
<mapper namespace="com.example.dao.UserMapper">
<!-- 开启二级缓存 -->
<cache
eviction="LRU" <!-- 回收策略:最近最少使用 -->
flushInterval="60000" <!-- 刷新间隔(毫秒),不写默认不清 -->
size="512" <!-- 最多缓存多少对象 -->
readOnly="true" <!-- 只读=true 性能更好,但不能修改对象 -->
/>
<!-- 或者最简写法(默认值很多) -->
<!-- <cache/> -->
<select id="selectById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>
</mapper>
或使用注解方式(MyBatis 3.4+ 支持)
@CacheNamespace(
eviction = LruCache.class,
flushInterval = 60000,
size = 512,
readOnly = true
)
public interface UserMapper { ... }
二级缓存什么时候生效?
- 同一个 namespace
- 相同的 SQL + 相同的参数
- 第一次查询后 SqlSession 正常 close()(数据才会被写到二级缓存)
- 后续其他 SqlSession 才能读到
二级缓存失效 / 刷新时机(非常重要)
- 本 namespace 下任意一条 增删改 语句执行(默认会清空整个 namespace 的二级缓存)
<cache>标签配置了 flushInterval,到时间自动清- 手动调用
session.clearCache()(会同时清一级+二级) - 缓存对象被 LRU/FIFO 等策略淘汰
一级 vs 二级 快速对比表
| 维度 | 一级缓存 | 二级缓存 |
|---|---|---|
| 作用域 | SqlSession | namespace(通常一个 Mapper) |
| 默认开启 | 是 | 否 |
| 共享范围 | 同一个会话 | 多个会话(跨 SqlSession) |
| 存储位置 | 内存(PerpetualCache) | 可自定义(Ehcache/Redis等) |
| 实体序列化要求 | 无 | 需要(Serializable) |
| 失效时机 | commit/rollback/close/update/delete 等 | update/delete + flushInterval 等 |
| 实际命中率 | 较高(同一次请求多次查) | 取决于业务(跨请求共享) |
| 风险点 | 脏读风险较低 | 脏读/缓存雪崩风险较高 |
真实项目中常见的做法(2025-2026 年视角)
- 大部分中小项目:只用一级缓存 + 局部热点数据用 @Cacheable(Spring Cache)或 Caffeine
- 中大型项目:
- 一级缓存照用
- 二级缓存基本不用原生的 PerpetualCache(太弱)
- 常用方案:
- mybatis-redis-cache(redis 做 L2)
- mybatis-caffeine-cache
- mybatis-ehcache(老项目常见)
- 或者直接在 service 层用 Spring Cache 注解
- 高并发读多写少:考虑引入 三级缓存(Redis / Memcached / 多级组合)
你现在项目里是怎么处理 MyBatis 缓存的?
是一级就够用,还是开了二级,或者直接扔到 Redis 了?可以聊聊你的实际做法~