主题
MyBatis 缓存机制
不变量
- 一级缓存:
SqlSession级别,默认开启,与配置无关——只要 SqlSession 在,一级缓存就在。 - 二级缓存:
Mapper(namespace)级别,需手动开启,多个 SqlSession 共享同一 Mapper 缓存。 - 两者底层都是
HashMap(PerpetualCache:一个 id 字符串 + 一个 HashMap)。
一级缓存(LocalCache)
- 数据结构:
BaseExecutor.localCache(PerpetualCache),Key 为CacheKey,Value 为查询结果。 CacheKey由 4 组共 5 个条件 决定两次查询是否"相同":<select>所在 Mapper 的namespace+ 标签idRowBounds的offset/limit<select>中定义的 SQL 语句- 输入参数的具体值(逐参数 update)
- 命中条件:满足以上条件且未设置
flushCache="true"→ 直接返回,不走 DB。 - 失效时机:执行
update/insert/delete(BaseExecutor.update第一行就clearLocalCache())会清空一级缓存,避免脏读。也可在<select>设flushCache="true"强制每次走 DB。 - 生命周期随
SqlSession关闭/回收而消失。
二级缓存
- 开启:全局
settings.cacheEnabled=true+ Mapper.xml 中<cache/>(或<cache-ref>共享别的 namespace)。 - 实体类必须实现
Serializable(因默认会装饰SerializedCache)。 - 跨 SqlSession 有效:SqlSession1 查询后
close(),SqlSession2 查同一数据不发出 SQL(但user1==user2为 false——二级缓存存的是"数据"而非"对象引用")。 - 增删改同样会清空二级缓存。
装饰器链(build 过程)
PerpetualCache(基础)被依次装饰: LruCache(淘汰策略,默认 LRU)→ ScheduledCache(按 flushInterval 定时清)→ SerializedCache(readOnly=false 时,要求序列化)→ LoggingCache(打印命中率)→ SynchronizedCache(方法加锁)→ BlockingCache(blocking=true 时,按 CacheKey 互斥)。
自定义/第三方缓存只会被装饰为
LoggingCache,因为淘汰、过期等能力由外部自己实现。
⚠️ 二级缓存的"无解"脏读陷阱
若一条跨表 SQL 定义在 MapperA,而其中某张表 tableB 的更新逻辑在 MapperB(另一 namespace):
MapperA查询并缓存结果(含tableB字段);MapperB更新了tableB相关字段;MapperA再次查询——因CacheKey的 4 组条件都没变,仍返回旧缓存值。 结论:启用二级缓存的前提是——所有对该数据的增删改查都在同一个 namespace 下。
常见误解
- "一级缓存是 DefaultSqlSession 里的" —— 错,定义在
Executor(默认SimpleExecutor)里,随 openSession 实例化。 - "二级缓存返回的是同一个对象" —— 错,返回的是数据副本(对象
==为 false)。 - "二级缓存默认开启" —— 错,必须显式配。
关联
- 上游:MyBatis一级缓存,二级缓存使用及源码分析
- 同域:MyBatis架构与插件机制(装饰器链与插件拦截器链同属"层层包装"思想)· 手写持久层框架(MyBatis原理铺垫)
- 域地图:Java后端