Skip to content

MyBatis 缓存机制

不变量

  • 一级缓存SqlSession 级别,默认开启,与配置无关——只要 SqlSession 在,一级缓存就在。
  • 二级缓存Mapper(namespace)级别,需手动开启,多个 SqlSession 共享同一 Mapper 缓存。
  • 两者底层都是 HashMapPerpetualCache:一个 id 字符串 + 一个 HashMap)。

一级缓存(LocalCache)

  • 数据结构:BaseExecutor.localCachePerpetualCache),Key 为 CacheKey,Value 为查询结果。
  • CacheKey4 组共 5 个条件 决定两次查询是否"相同":
    1. <select> 所在 Mapper 的 namespace + 标签 id
    2. RowBoundsoffset/limit
    3. <select> 中定义的 SQL 语句
    4. 输入参数的具体值(逐参数 update)
  • 命中条件:满足以上条件且未设置 flushCache="true" → 直接返回,不走 DB。
  • 失效时机:执行 update/insert/deleteBaseExecutor.update 第一行就 clearLocalCache())会清空一级缓存,避免脏读。也可在 <select>flushCache="true" 强制每次走 DB。
  • 生命周期随 SqlSession 关闭/回收而消失。

二级缓存

  • 开启:全局 settings.cacheEnabled=true + Mapper.xml 中 <cache/>(或 <cache-ref> 共享别的 namespace)。
  • 实体类必须实现 Serializable(因默认会装饰 SerializedCache)。
  • 跨 SqlSession 有效:SqlSession1 查询后 close(),SqlSession2 查同一数据不发出 SQL(但 user1==user2false——二级缓存存的是"数据"而非"对象引用")。
  • 增删改同样会清空二级缓存。

装饰器链(build 过程)

PerpetualCache(基础)被依次装饰: LruCache(淘汰策略,默认 LRU)→ ScheduledCache(按 flushInterval 定时清)→ SerializedCache(readOnly=false 时,要求序列化)→ LoggingCache(打印命中率)→ SynchronizedCache(方法加锁)→ BlockingCacheblocking=true 时,按 CacheKey 互斥)。

自定义/第三方缓存只会被装饰为 LoggingCache,因为淘汰、过期等能力由外部自己实现。

⚠️ 二级缓存的"无解"脏读陷阱

若一条跨表 SQL 定义在 MapperA,而其中某张表 tableB 的更新逻辑在 MapperB(另一 namespace):

  1. MapperA 查询并缓存结果(含 tableB 字段);
  2. MapperB 更新了 tableB 相关字段;
  3. MapperA 再次查询——因 CacheKey 的 4 组条件都没变,仍返回旧缓存值。 结论:启用二级缓存的前提是——所有对该数据的增删改查都在同一个 namespace 下。

常见误解

  • "一级缓存是 DefaultSqlSession 里的" —— 错,定义在 Executor(默认 SimpleExecutor)里,随 openSession 实例化。
  • "二级缓存返回的是同一个对象" —— 错,返回的是数据副本(对象 == 为 false)。
  • "二级缓存默认开启" —— 错,必须显式配。

关联

最近更新