MyBatis 的缓存机制
一句话总结
MyBatis 有两级缓存:
- 一级缓存(Local Cache) :
SqlSession级别,默认开启 ;同一会话内相同查询可复用结果;执行更新、clearCache()、commit/rollback或会话结束会触发清理。 - 二级缓存(Second Level Cache) :
Mapper(namespace)级别,需 全局开启 + Mapper 开启 ;跨SqlSession共享。查询结果通常在会话提交/关闭时才写入二级缓存;更新会按 namespace 刷新相关缓存。
结论:一级缓存解决"同一会话内重复查" ;二级缓存解决"跨会话读多写少",但要关注一致性与线程/分布式问题。
1. 两级缓存全景图
| 维度 | 一级缓存 | 二级缓存 |
|---|---|---|
| 作用域 | SqlSession |
Mapper namespace |
| 默认 | 开启 | 关闭(需配置) |
| 共享性 | 不共享 | 同 namespace 的多个 SqlSession 共享 |
| 写入时机 | 查询后立刻放入当前会话缓存 | 通常在 commit/close 后由会话把数据刷入二级缓存 |
| 失效时机 | 更新/清理/提交回滚/会话结束 | 更新(默认会 flush)、到期、手动清理 |
| 典型问题 | 会话长导致内存占用;误以为跨会话有效 | 脏读/一致性;对象需可序列化;分布式需要外部缓存 |
2. 一级缓存(Local Cache)
2.1 核心特点
- 范围 :同一个
SqlSession。 - 缓存 key :MyBatis 会基于
MappedStatement、SQL、参数、分页、环境等生成CacheKey。 - 默认开启:无需配置。
2.2 什么时候命中?
同一 SqlSession 中:
- SQL 相同
- 参数相同
RowBounds等影响结果的条件相同
则第二次查询直接返回缓存结果。
2.3 什么时候失效/清理?
常见触发:
- 执行
INSERT/UPDATE/DELETE - 手动调用
sqlSession.clearCache() commit()/rollback()(会清理本地缓存,避免事务边界引发脏数据)close()
注意:实际行为与执行器(Executor)实现有关,但面试记住以上规则基本够用。
2.4 关键配置:localCacheScope
在 mybatis-config.xml:
xml
<settings>
<!-- 默认 SESSION:一级缓存跨 statement 生效(同一 SqlSession 内) -->
<setting name="localCacheScope" value="SESSION"/>
<!-- 若设为 STATEMENT:每次 statement 执行完就清掉本地缓存(几乎等于关闭一级缓存) -->
<!-- <setting name="localCacheScope" value="STATEMENT"/> -->
</settings>
SESSION(默认):一级缓存效果最明显。STATEMENT:每次查询执行完清理,适合对一致性极敏感、且不希望复用本地结果的场景。
2.5 一级缓存示例(命中与失效)
java
try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
User u1 = mapper.selectById(1);
User u2 = mapper.selectById(1); // 命中一级缓存
mapper.updateName(1, "newName"); // 触发清理(同 namespace 的更新会导致缓存失效)
User u3 = mapper.selectById(1); // 重新查库
session.commit();
}
3. 二级缓存(Second Level Cache)
3.1 开启条件(缺一不可)
- 全局开启:
xml
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
- Mapper 开启(二选一):
- 在 Mapper XML 中加
<cache/> - 或使用
@CacheNamespace注解(不常用,面试可提)
示例:
xml
<mapper namespace="com.example.UserMapper">
<cache eviction="LRU" flushInterval="60000" size="1024" readOnly="false"/>
<select id="selectById" resultType="User" useCache="true">
SELECT * FROM user WHERE id = #{id}
</select>
</mapper>
3.2 关键参数(会背更加分)
| 参数 | 含义 | 常见取值 |
|---|---|---|
eviction |
淘汰策略 | LRU(默认)、FIFO、SOFT、WEAK |
flushInterval |
自动刷新间隔(毫秒) | 不配则不定时刷新 |
size |
缓存条目上限(近似理解) | 如 1024 |
readOnly |
是否只读 | true 返回同一实例(快但风险大);false 返回拷贝(更安全) |
3.3 二级缓存写入时机(高频误区)
二级缓存并不是"查完立刻共享出去"。通常流程是:
SqlSession查询:先查一级缓存;未命中再查二级缓存;再未命中查 DB。- 查 DB 得到结果:先放到当前会话一级缓存。
- 当会话
commit()或close():才把本会话中可缓存的数据刷入二级缓存。
因此:
- 如果你查完不提交/不关闭会话,其他
SqlSession不一定能命中二级缓存。
3.4 二级缓存失效规则
- 默认情况下,同 namespace 下的
INSERT/UPDATE/DELETE会触发缓存刷新。 - 对单条语句可用:
flushCache="true":执行后刷新缓存(更新语句默认就是 true)useCache="false":本次查询不使用二级缓存
4. 同时启用时的查询顺序
- 先查 一级缓存(当前 SqlSession)
- 再查 二级缓存(namespace 级)
- 最后查 数据库
5. 一致性与常见问题
5.1 脏读/过期数据
二级缓存适合"读多写少、允许一定延迟"的数据(配置表、字典表、热点但更新少的数据)。
若并发更新频繁:
- 可能出现缓存返回旧值(取决于刷新策略、事务提交时机以及调用链)
- 建议:
- 缩小/关闭二级缓存范围
- 对关键查询
useCache=false - 或把一致性问题交给更专业的缓存体系(Redis + 失效策略/订阅通知等)
5.2 序列化问题
- 二级缓存跨会话共享,默认实现通常要求缓存对象可被序列化。
- 实体类建议实现
Serializable(或使用支持对象拷贝/序列化的缓存实现)。
5.3 分布式环境
MyBatis 自带二级缓存多为单机内存缓存。
- 多实例部署时,各实例二级缓存互不感知,会导致数据不一致。
- 解决:集成分布式缓存(Redis/Ehcache 集群等)或关闭二级缓存。
6. 扩展:自定义/第三方缓存
MyBatis 的二级缓存基于 Cache 接口,默认实现常见有 PerpetualCache + 各种装饰器(如 LRU)。
若接入第三方缓存(以 Redis 为例),Mapper 可配置:
xml
<cache type="org.mybatis.caches.redis.RedisCache"/>
7. 最佳实践(直接背)
- 默认依赖一级缓存即可:它天然符合一次请求/一次会话内复用。
- 二级缓存只用于读多写少、且能接受一定延迟的数据;热点写多的表慎用。
- 线上遇到"查到旧数据/偶现不一致",优先排查:
- 是否启用了二级缓存
- 会话是否未提交导致缓存未刷入
- 更新语句是否按 namespace 正常 flush
- 是否分布式多实例导致缓存不一致
- 分布式场景更推荐:业务侧缓存(Redis)+ 明确失效策略,而不是依赖 MyBatis 二级缓存。
8. 高频面试问答
Q1:一级缓存为什么默认开启?
因为它仅在单个 SqlSession 内生效,能减少同会话内重复查询,收益大且一致性风险相对可控。
Q2:二级缓存为什么要提交/关闭才写入?
为了避免把"未提交事务中的数据"共享出去导致更严重的一致性问题;因此通常在事务边界(commit/close)再刷入。
Q3:useCache 和 flushCache 有什么用?
useCache=false:本次查询不走二级缓存。flushCache=true:执行后刷新缓存(更新默认开启)。
Q4:生产上建议开二级缓存吗?
取决于业务:读多写少、低一致性要求、单机或有一致性方案时可考虑;否则更推荐 Redis 等集中式缓存并配合失效策略。