MyBatis 一级缓存与二级缓存的原理是什么?有哪些使用坑?
结论先行:MyBatis 一级缓存是 SqlSession 作用域的本地 Map,默认开启,同一会话内相同查询命中缓存;二级缓存是 Mapper(namespace) 作用域、跨 SqlSession 共享,需显式开启并自行承担一致性风险。一级缓存“同查询同结果”看似无害,却常与 Spring 事务、跨会话场景叠加出脏读/陈旧数据问题。
一、两级缓存对比
| 维度 | 一级缓存(Local Cache) | 二级缓存 |
|---|---|---|
| 作用域 | SqlSession(默认 SESSION) | namespace(跨 SqlSession) |
| 默认状态 | 开启 | 关闭,需 <cache/> 开启 |
| 存储 | SqlSession 内 Map | 全局缓存(可配 Redis/ehcache) |
| 失效 | 会话提交/关闭/clearCache、更新语句 | namespace 内增删改清空该缓存 |
| 风险 | 事务内读到本会话旧值 | 跨 namespace 脏数据、集群不一致 |
二、一级缓存的关键机制
- 缓存键 = statementId + SQL + 参数 + RowBounds;
- 查询先查缓存,未命中才查库并回填;
- 同 SqlSession 内执行 update/insert/delete 会清空缓存(避免脏读);
- 坑点一:Spring 集成时若未开启事务,每次 Mapper 调用新建 SqlSession,一级缓存形同虚设;
- 坑点二:开启事务时整个事务共用一个 SqlSession,先查后改可能读到“改前”的旧结果(可调 localCacheScope=STATEMENT 或强制刷新)。
三、二级缓存使用与坑
<mapper namespace="com.demo.mapper.UserMapper">
<cache eviction="LRU" flushInterval="60000" size="1024" readOnly="false"/>
<!-- 关联表更新语句上补 flushCache="true",防止跨表脏读 -->
<update id="updateOrder" flushCache="true">...</update>
</mapper>
- 共享缓存的对象需可序列化(readOnly=false 时);
- 经典坑:多表 join 或别处更新关联表,只清了本 namespace 缓存 → 读到旧数据,需 cache-ref 共享或合理拆分;
- 集群/多实例:本地二级缓存不共享,必须接分布式缓存(如 mybatis-redis)或干脆关闭;
- 与事务配合:提交后才把数据放入二级缓存,回滚不会污染缓存。
常见追问 / 记忆点
- 追问:一级缓存会不会读到已提交的其他会话数据?答:不会跨会话,一级缓存只对本 SqlSession 可见,天然无跨会话问题。
- 追问:线上要不要开二级缓存?答:单机低写场景收益有限、一致性风险高;高并发读多写少且有分布式方案才考虑。
- 记忆点:一级=会话内、自动开;二级=namespace、手动开;集群一致性是二级缓存最大敌人。