MyBatis 缓存机制深度剖析与实践指南
MyBatis 的缓存机制是提升数据库访问性能的核心手段,通过减少重复查询来降低数据库压力。深入解析一级缓存、二级缓存的工作原理、适用场景、配置技巧及最佳实践,帮助你在实际项目中合理运用缓存提升系统性能。
缓存机制整体架构
MyBatis 缓存分为一级缓存和二级缓存,两者在作用范围、生命周期和使用方式上有显著区别,但协同工作形成完整的缓存体系:
| 维度 | 一级缓存(Local Cache) | 二级缓存(Global Cache) |
|---|---|---|
| 作用范围 | SqlSession 级别(单次会话) | Mapper 接口(namespace)级别 |
| 默认状态 | 自动开启,无法关闭 | 需手动开启(全局 + 映射文件配置) |
| 数据存储 | 内存 HashMap(无持久化) | 可配置(内存 / 第三方缓存如 Redis) |
| 共享性 | 仅当前 SqlSession 可见 | 同 SqlSessionFactory 下所有 SqlSession 共享 |
| 失效触发 | SqlSession 关闭 / 提交 / 增删改操作 | 同 namespace 下增删改操作 / 缓存过期 |
MyBatis 缓存查询流程如下:
- 发起查询时,先检查二级缓存(若开启);
- 二级缓存未命中,检查一级缓存;
- 一级缓存未命中,执行数据库查询;
- 查询结果依次写入一级缓存和二级缓存(若开启)。
一级缓存:SqlSession 级别的本地缓存
一级缓存是 MyBatis 内置的基础缓存,默认开启且无需额外配置,其核心作用是在单次数据库会话中复用查询结果。
工作原理
- 存储结构:每个
SqlSession内部维护一个HashMap作为缓存容器,键为CacheKey,值为查询结果。 - CacheKey 生成逻辑:由以下因素共同决定(任意一项不同则 CacheKey 不同):
- MappedStatement 的 ID(即
namespace + methodName); - 分页参数(offset/limit);
- SQL 语句本身;
- 传入的参数值;
- 数据库环境 ID(environment.id)。
- MappedStatement 的 ID(即
- 生命周期:与
SqlSession一致,随SqlSession关闭而销毁。
一级缓存命中与失效示例
示例 1:一级缓存命中(同 SqlSession 内重复查询)
1 | SqlSession session = sqlSessionFactory.openSession(); |
示例 2:一级缓存失效场景
1 | SqlSession session = sqlSessionFactory.openSession(); |
一级缓存配置与控制
禁用一级缓存:通过全局配置
localCacheScope控制,默认值为SESSION(开启一级缓存),设置为STATEMENT可禁用(每次查询后立即清空缓存):1
2
3
4<settings>
<!-- 禁用一级缓存:每次查询都走数据库 -->
<setting name="localCacheScope" value="STATEMENT"/>
</settings>适用场景:
- 单次会话内需要重复查询相同数据(如表单校验、数据展示);
- 避免短时间内重复访问数据库,减轻瞬时压力。
二级缓存:跨 SqlSession 的全局缓存
二级缓存是基于 namespace(Mapper 接口)的全局缓存,可跨 SqlSession 共享数据,适合查询频繁、更新较少的数据(如字典表、配置表)。
开启与配置二级缓存
二级缓存需要两步配置才能生效:
步骤 1:全局开启二级缓存(默认已开启,可显式配置)
1 | <settings> |
步骤 2:在 Mapper 映射文件中声明缓存
1 | <!-- UserMapper.xml --> |
二级缓存核心配置解析
| 配置项 | 作用说明 |
|---|---|
eviction |
缓存回收策略: - LRU(最近最少使用):移除最长时间未访问的条目(推荐); - FIFO(先进先出):按插入顺序移除最早条目; - SOFT(软引用):基于 JVM 软引用规则回收(内存不足时); - WEAK(弱引用):基于 JVM 弱引用规则回收(GC 时立即回收)。 |
flushInterval |
缓存自动清空间隔(毫秒),默认 null(永不自动清空),适合定时更新的数据。 |
size |
最大缓存条目数,需根据内存大小调整(避免 OOM)。 |
readOnly |
- true:返回缓存对象的引用(性能高,线程不安全); - false:返回对象的序列化克隆(性能略低,线程安全,默认)。 |
blocking |
缓存未命中时是否阻塞(true 则等待其他线程加载完成,避免缓存击穿)。 |
二级缓存工作流程与示例
工作流程:
SqlSession1查询数据 → 写入一级缓存;SqlSession1提交(commit())→ 一级缓存数据写入二级缓存;SqlSession2查询相同数据 → 直接从二级缓存获取。
示例:二级缓存命中(跨 SqlSession 共享)
1 | // 实体类必须实现 Serializable(readOnly=false 时需要序列化) |
二级缓存失效场景
未提交事务:
SqlSession未调用commit(),一级缓存数据不会写入二级缓存;同 namespace 下的增删改操作:增删改标签默认
flushCache="true",执行后会清空当前 namespace 的二级缓存;查询标签配置
useCache="false":强制禁用当前查询的二级缓存(一级缓存仍生效):1
2
3<select id="selectById" useCache="false"> <!-- 不使用二级缓存 -->
SELECT * FROM user WHERE id = #{id}
</select>跨 namespace 操作:二级缓存是 namespace 隔离的,其他 Mapper 的操作不会影响当前缓存。
自定义二级缓存:集成第三方缓存
MyBatis 二级缓存支持自定义实现,通过实现 org.apache.ibatis.cache.Cache 接口,可集成 Redis、EHCache 等专业缓存框架(解决内存缓存的容量限制和持久化问题)。
示例:集成 Redis 作为二级缓存
步骤 1:实现 Cache 接口
1 | public class RedisCache implements Cache { |
步骤 2:在 Mapper 中引用自定义缓存
1 | <mapper namespace="com.example.mapper.UserMapper"> |
缓存使用最佳实践
1. 一级缓存使用建议
- 默认保留:一级缓存无需额外配置,能有效优化单次会话内的重复查询;
- 避免长会话:长时间持有
SqlSession会导致一级缓存膨胀,建议用完即关; - 禁用场景:实时性要求极高的查询(如股票行情),可通过
localCacheScope=STATEMENT禁用。
2. 二级缓存使用建议
- 适合场景:查询频繁、更新较少的数据(如字典表、地区表);
- 不适合场景:实时性要求高的数据(如订单状态)、频繁更新的表;
- 序列化要求:
readOnly=false时,实体类必须实现Serializable接口; - 缓存粒度:避免在大表对应的 Mapper 中开启二级缓存(缓存条目过多导致性能下降)。
3. 缓存问题规避
- 缓存穿透:对不存在的 key 频繁查询(如恶意攻击),可通过布隆过滤器过滤无效 key;
- 缓存击穿:热点 key 过期瞬间的大量并发查询,可配置
blocking=true或设置永不过期; - 缓存雪崩:大量缓存同时过期,可设置随机过期时间(如
expireTime + 随机数)避免集中失效。
4. 缓存配置速查表
| 配置项 | 作用范围 | 默认值 | 说明 |
|---|---|---|---|
cacheEnabled |
全局(二级缓存) | true |
开启 / 关闭所有二级缓存 |
localCacheScope |
全局(一级缓存) | SESSION |
SESSION(开启)/STATEMENT(禁用) |
<cache> |
Mapper 级别 | 无 | 声明二级缓存并配置参数 |
useCache |
查询标签 | true |
当前查询是否使用二级缓存 |
flushCache |
增删改标签 | true |
执行后是否清空一级 + 二级缓存 |
flushCache |
查询标签 | false |
执行后是否清空缓存(true 则禁用缓存) |
总结
MyBatis 缓存机制通过一级缓存(SqlSession 级别)和二级缓存(namespace 级别)的协同,有效减少了数据库访问次数。实际开发中,应根据数据特性选择合适的缓存策略:
- 短期会话内的重复查询依赖一级缓存;
- 跨会话共享的低频更新数据使用二级缓存,并优先集成 Redis 等第三方缓存框架;
- 实时性要求高或频繁更新的数据应禁用缓存,避免数据不一致