4. MyBatis 二级缓存深度解析

260 阅读19分钟

MyBatis 二级缓存深度解析

上一章中,我们介绍了Executor的三种实现,还有一种实现没有介绍,那就是CachingExecutor,用于实现二级缓存。本章我们介绍一下Mybatis的二级缓存是如何实现的。在详细解读之前,我们先简单看下,二级缓存要如何使用。

一、二级缓存的开启与使用

二级缓存默认关闭(全局开关默认开启,每个Mapper默认关闭),需通过全局配置与Mapper配置共同启用。其核心设计是 “按需启用”,确保缓存只作用于需要的业务场景。

1. 全局开关:开启二级缓存支持

MyBatis 通过cacheEnabled全局配置控制二级缓存总开关,默认值为true(即默认支持二级缓存,但需配合 Mapper 配置才会生效)。可在mybatis-config.xml中显式配置:

    <configuration>
      <settings>
        <!-- 开启二级缓存支持(默认true,可省略) -->
        <setting name="cacheEnabled" value="true"/>
      </settings>
    </configuration>

注意:若cacheEnabled设为false,将禁用所有二级缓存,即使Mapper中配置了<cache>也无效。

2. Mapper 级别配置:指定缓存策略

二级缓存的精细控制在 Mapper 层,通过<cache>标签(XML)或@CacheNamespace注解(接口)启用,同时可配置缓存的过期策略、容量等参数。

(1)XML Mapper 配置示例
    <!-- UserMapper.xml -->
    <mapper namespace="com.example.mapper.UserMapper">
      <!-- 开启二级缓存,配置缓存策略 -->
      <cache 
        eviction="LRU"        <!-- 淘汰策略LRU最近最少使用-->
        flushInterval="60000" <!-- 自动刷新间隔(毫秒),60秒 -->
        size="1024"           <!-- 最大缓存条目 -->
        readOnly="false"/>    <!-- 是否只读:false支持读写,false时写入缓存是会做序列化,读取时做反序列化,也就是做个深拷贝,这样即使修改缓存也不会有副作用 -->

      <!-- 查询语句:默认使用二级缓存 -->
      <select id="selectUserById" resultType="User">
        SELECT id, name, age FROM user WHERE id = #{id}
      </select>

      <!-- 更新语句:默认会刷新缓存(清空当前namespace的二级缓存) -->
      <update id="updateUser">
        UPDATE user SET name = #{name} WHERE id = #{id}
      </update>
    </mapper>
(2)注解配置示例
    // UserMapper.java
    @CacheNamespace(
      eviction = LruCache.class,    // 淘汰策略
      flushInterval = 60000,        // 自动刷新间隔
      size = 1024,                  // 最大条目
      readWrite = true              // 读写模式
    )
    public interface UserMapper {
      @Select("SELECT id, name, age FROM user WHERE id = #{id}")
      User selectUserById(Long id);

      @Update("UPDATE user SET name = #{name} WHERE id = #{id}")
      int updateUser(User user);
    }

3. 序列化

一般readOnly属性,都会设置成false,除非你能保证读取缓存后一定不会修改缓存而引起副作用。因此实体类实现Serializable接口:

    public class User implements Serializable {
      private Long id;
      private String name;
      private Integer age;
      // getter/setter
    }

大功告成,通过以上配置,就可以愉快的使用Mybatis的二级缓存了。

二、二级缓存的核心问题与解决方案

在分析源码之前,我们先讨论下,二级缓存有哪些挑战。Mybatis的二级缓存是namespace级别的,或者说是Mapper级别的,是跨SqlSession的。SqlSession本身是不支持多线程的,因此SqlSession级别的一级缓存是不需要考虑并发的。但在二级缓存场景下,必须考虑并发问题,这也导致二级缓存变得复杂起来。

1. 缓存并发问题

问题:二级缓存可能存在并发的读写,因此之前PerpetualCache直接使用HashMap实现的缓存,会有并发问题。

解决方案:Mybatis采用装饰者模式,使用SynchronizedCacheCache做一层装饰,以实现Cache读取的同步。SynchronizedCache实现也比较简单,缓存的读写方法,都使用synchronized修饰,以达到同步读取的效果。

2. 事务可见性问题

问题:假设我们简单的使用一级缓存的方式实现二级缓存,那么在多线程的场景下,则可能:

sequenceDiagram
    participant 线程1
    participant 线程1的SqlSession
    participant 二级缓存
    participant 数据库
    participant 线程2
    participant 线程2的SqlSession

    线程1->>线程1的SqlSession: 开启SqlSession(开启事务)
    线程1->>线程1的SqlSession: 执行update(id=1) 【更新数据】
    线程1的SqlSession->>数据库: 执行UPDATE语句
    database-->>线程1的SqlSession: 更新成功(事务未提交)
    线程1的SqlSession->>二级缓存: 清空当前namespace缓存(默认行为)
    线程1->>线程1的SqlSession: 执行select(id=1) 【查询更新后的数据】
    线程1的SqlSession->>数据库: 执行SELECT语句(因缓存已清空)
    database-->>线程1的SqlSession: 返回id=1的新值(未提交的事务内可见)
    
    Note over 线程1: 错误场景:直接将查询结果写入二级缓存
    线程1的SqlSession->>二级缓存: 写入id=1的新值(未提交数据)
    线程1的SqlSession-->>线程1: 返回查询结果

    线程2->>线程2的SqlSession: 开启SqlSession
    线程2->>线程2的SqlSession: 执行select(id=1) 【查询同一数据】
    线程2的SqlSession->>二级缓存: 命中缓存
    二级缓存-->>线程2的SqlSession: 返回id=1的新值(脏数据)
    线程2的SqlSession-->>线程2: 读取到未提交的脏数据

    Note over 线程1: 最终线程1回滚事务...
    线程1->>线程1的SqlSession: 事务回滚
    线程1的SqlSession->>数据库: 回滚update操作
    database-->>线程1的SqlSession: 回滚成功
    Note over 二级缓存,线程2: 问题:线程2已读取到缓存中的脏数据,且不会自动刷新

简单的说,如果线程A更新了id=1的数据之后,又查询了这条数据,就可以查出新修改的数据,如果直接放入二级缓存中,则存在问题:

  1. 直接把未提交的数据放到了二级缓存中,导致其他线程可见,破坏了数据库的隔离级别,导致可以读到未提交的数据。
  2. 事务可能回滚,造成二级缓存存在脏数据。

解决方案:还是使用装饰者模式,使用TransactionalCacheCache上再套一层事务缓存。查询之后的数据并不会立即更新达到二级缓存,而是先暂存在TransactionalCache上,提交后再更新到二级缓存。回滚则直接丢弃。

3. 缓存一致性:如何保证缓存与数据库同步

问题:当数据被更新(如update/insert/delete)时,二级缓存中的旧数据需及时失效,否则会导致查询结果不一致。

解决方案

  1. 执行更新操作时,清空缓存。
    • 默认策略:MyBatis 默认对update/insert/delete操作触发缓存刷新(清空当前Mapper命名空间的二级缓存)。

    • 手动控制:也可通过@Options(flushCache = FlushCachePolicy.FALSE)<select flushCache="true">手动控制Mapper方法是否刷新缓存。

  2. 周期清理,可以设置flushInterval参数,设置缓存清理的时间间隔。实现类是ScheduledCache,还是装饰器模式,每当get/put等方法被调用时,都会触发时间间隔的检查,假如超过了指定时间,清空缓存。

4. 缓存淘汰策略

问题:缓存不可能无限大,当缓存超过一定阈值时,需要淘汰策略。

解决方案:Mybatis提供了四种淘汰策略,LRU和FIFO可以配置size参数使用,当缓存数量超过阈值时,触发清理。SOFT、WEAK策略,则依赖JVM的垃圾回收。都是基于装饰者模式实现的。

策略含义适用场景实现类
LRU最近最少使用(Least Recently Used):移除最长时间未被使用的条目大部分场景,优先保留热点数据LruCache
FIFO先进先出(First In First Out):按插入顺序移除最早的条目数据访问顺序固定的场景FifoCache
SOFT软引用:基于 JVM 的软引用机制,内存不足时移除允许缓存溢出的场景(依赖 JVM GC)SoftCache
WEAK弱引用:基于 JVM 的弱引用机制,GC 时立即移除缓存数据生命周期短的场景WeakCache

4. 缓存对象本身的读写问题

问题:缓存可以跨SqlSession共享,也就是可以多线程共享,那么一个线程获取到缓存后,如果修改,则会直接修改缓存中的数据,进而影响到其他线程。

解决方案:深拷贝,实现类为SerializedCache,还是装饰者模式,写入缓存是进行序列化,读取时反序列化,相当于做了一次深拷贝。可以通过readOnly参数控制,如果为true,则不进行序列化,也就是用户自己注意,从缓存中读取的数据是只读的,不会被修改。

三、二级缓存的源码实现

从上文的解决方案中,可以发现,Mybatis非常钟爱装饰器模式(或者叫做责任链、代理模式,都一样),没有什么问题,是加一层装饰器不能解决的。

1. CachingExecutor & 装饰器模式

装饰器模式(Decorator Pattern)的核心是在不修改原有对象结构和代码的前提下,通过 “包装” 方式动态扩展对象功能。MyBatis的CachingExecutor是这一模式的经典实现 —— 它通过装饰基础 Executor(如 SimpleExecutor),为其添加二级缓存相关功能,同时保持 Executor 接口的一致性。以下结合源码详细解析。

装饰器模式的核心要素

装饰器模式有三个核心组成部分,在 CachingExecutor 中体现得淋漓尽致:

  1. 抽象组件(Component) :定义统一接口,是被装饰者和装饰者的共同父类 / 接口。
    在 MyBatis 中对应 Executor 接口,定义了数据库操作的核心方法(queryupdatecommit 等)。
  2. 具体组件(ConcreteComponent) :实现抽象组件的原始对象,是被装饰的目标。
    在 MyBatis 中对应 SimpleExecutorReuseExecutorBatchExecutor 等基础执行器,负责实际的 SQL 执行逻辑。
  3. 装饰器(Decorator) :实现抽象组件接口,持有具体组件的引用,通过包装具体组件扩展功能。

在 MyBatis 中 CachingExecutor 就是装饰器,它持有基础 Executor 的引用,同时也实现了 Executor 接口,这样,就可以在Executor接口基础上添加缓存相关逻辑:

// 抽象组件:Executor接口
public interface Executor {
  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
  int update(MappedStatement ms, Object parameter) throws SQLException;
  void commit(boolean required) throws SQLException;
  void rollback(boolean required) throws SQLException;
  // 其他核心方法...
}

// 装饰器:CachingExecutor实现Executor接口
public class CachingExecutor implements Executor {
  // 持有被装饰的基础Executor(具体组件)
  private final Executor delegate;
  // 缓存管理器,用于管理二级缓存
  private final TransactionalCacheManager tcm = new TransactionalCacheManager();

  // 构造函数:接收被装饰的Executor实例
  public CachingExecutor(Executor delegate) {
    this.delegate = delegate;
    delegate.setExecutorWrapper(this); // 让基础Executor知道自己被装饰了
  }

  // 实现Executor接口的所有方法...
}

下面我们简单看下CachingExecutor是如何在原有的Executor的基础上,实现二级缓存的。

(1)query 方法:查询时先查缓存,未命中再查数据库
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
  // 1. 获取SQL和参数,生成缓存key
  BoundSql boundSql = ms.getBoundSql(parameter);
  CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
  // 2. 调用带缓存key的重载方法
  return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  // 3. 获取当前Mapper的二级缓存
  Cache cache = ms.getCache();
  if (cache != null) {
    // 4. 若需要,先刷新缓存(如配置了flushCache=true)
    flushCacheIfRequired(ms);
    // 5. 若启用缓存且无结果处理器,尝试从缓存获取
    if (ms.isUseCache() && resultHandler == null) {
      // 校验是否有输出参数(存储过程场景)
      ensureNoOutParams(ms, boundSql);
      // 6. 从二级缓存获取数据
      @SuppressWarnings("unchecked")
      List<E> list = (List<E>) tcm.getObject(cache, key);
      // 7. 缓存未命中,调用基础Executor的query方法查数据库
      if (list == null) {
        list = delegate.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
        // 8. 将数据库查询结果存入二级缓存
        tcm.putObject(cache, key, list);
      }
      return list;
    }
  }
  // 9. 若未启用缓存,直接调用基础Executor查数据库
  return delegate.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

装饰逻辑
在调用基础 Executor 的 query 方法(delegate.query(...))前,先检查二级缓存;若缓存未命中,查询数据库后再将结果存入缓存。这一过程完全不修改 SimpleExecutor 等基础执行器的代码,仅通过装饰器扩展了 “缓存查询” 功能。

(2)update 方法:更新时刷新缓存,保证数据一致性
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
  // 1. 若需要,先刷新缓存(增删改默认flushCache=true)
  flushCacheIfRequired(ms);
  // 2. 调用基础Executor的update方法执行实际更新
  return delegate.update(ms, parameter);
}

// 刷新缓存的具体逻辑:注意这里与查询是清缓存是一个方法,具体是否清理还要看用户配置,默认修改清缓存,查询不清除缓存,用户可以通过flushCache配置改变默认行为
private void flushCacheIfRequired(MappedStatement ms) {
  Cache cache = ms.getCache();
  // 若缓存存在且当前SQL需要刷新缓存(如增删改或配置了flushCache=true)
  if (cache != null && ms.isFlushCacheRequired()) {
    tcm.clear(cache); // 清空二级缓存
  }
}

装饰逻辑
在调用基础 Executor 的 update 方法前,先根据配置清空二级缓存,避免缓存中留存旧数据。这一 “刷新缓存” 的功能同样是通过装饰器添加,不影响原始 update 方法的执行逻辑。

(3)commit/rollback 方法:同步事务状态与缓存
@Override
public void commit(boolean required) throws SQLException {
  // 1. 调用基础Executor的commit方法提交事务
  delegate.commit(required);
  // 2. 提交事务时,同步二级缓存(将暂存的缓存数据写入实际缓存)
  tcm.commit();
}

@Override
public void rollback(boolean required) throws SQLException {
  try {
    // 1. 调用基础Executor的rollback方法回滚事务
    delegate.rollback(required);
  } finally {
    if (required) {
      // 2. 回滚事务时,清理二级缓存中暂存的无效数据
      tcm.rollback();
    }
  }
}

装饰逻辑
在事务提交 / 回滚时,通过 TransactionalCacheManager 同步二级缓存的状态(提交时写入缓存,回滚时丢弃无效数据),确保缓存与数据库事务的一致性。这一功能是对基础事务操作的扩展。

2. 事务缓存管理:TransactionalCache 与 TransactionalCacheManager

回顾下CachingExecutor的源码,先从MappedStatement(ms)中获取缓存,如果存在,使用TransactionalCacheManager(tcm)获取缓存。

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  // 3. 获取当前Mapper的二级缓存
  Cache cache = ms.getCache();
  if (cache != null) {
    // 4. 若需要,先刷新缓存(如配置了flushCache=true)
    flushCacheIfRequired(ms);
    // 5. 若启用缓存且无结果处理器,尝试从缓存获取
    if (ms.isUseCache() && resultHandler == null) {
      // 校验是否有输出参数(存储过程场景)
      ensureNoOutParams(ms, boundSql);
      // 6. 从二级缓存获取数据
      @SuppressWarnings("unchecked")
      List<E> list = (List<E>) tcm.getObject(cache, key);
      // 7. 缓存未命中,调用基础Executor的query方法查数据库
      if (list == null) {
        list = delegate.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
        // 8. 将数据库查询结果存入二级缓存
        tcm.putObject(cache, key, list);
      }
      return list;
    }
  }
  // 9. 若未启用缓存,直接调用基础Executor查数据库
  return delegate.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
TransactionalCacheManager

看到这你可能有点疑惑,已经通过MappedStatement(ms)中获取了二级缓存,为什么还要在通过TransactionalCacheManager去存取缓存呢?

还记得我们前文提到的事务可见性的问题么,Mybatis需要在事务提交后,才能更新缓存,因此需要暂存下未提交的数据,这是通过TransactionalCache装饰Cache实现的。二级缓存是Mapper级别的,而一个ExecutorExecutor的生命周期与SqlSession是一致的)可能会执行多个Mapper,它与二级缓存是一对多的关系,因此需要维护CacheTransactionalCache的关系,这是通过也TransactionalCacheManager实现的:

    public class TransactionalCacheManager {
      //维护Cache与TransactionalCache的关系
      private final Map<Cache, TransactionalCache> TransactionalCaches = new HashMap<>();

      // 从缓存获取数据(实际从TransactionalCache获取)
      public Object getObject(Cache cache, CacheKey key) {
        return getTransactionalCache(cache).getObject(key);
      }

      // 向缓存存入数据(实际存入TransactionalCache的临时容器)
      public void putObject(Cache cache, CacheKey key, Object value) {
        getTransactionalCache(cache).putObject(key, value);
      }

      // 提交事务:将TransactionalCache中的临时数据刷入实际缓存
      public void commit() {
        for (TransactionalCache txCache : TransactionalCaches.values()) {
          txCache.commit();
        }
      }

      // 回滚事务:清空TransactionalCache中的临时数据
      public void rollback() {
        for (TransactionalCache txCache : TransactionalCaches.values()) {
          txCache.rollback();
        }
      }

      // 获取缓存对应的TransactionalCache(懒创建)
      private TransactionalCache getTransactionalCache(Cache cache) {
        return TransactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
      }
    }
TransactionalCache

TransactionalCache是事务缓存的核心实现。

  1. 内部属性
//被代理对象
private final Cache delegate;
//记录clear方法是否被调用,如果被调用,不会立即清空,而是提交时才会清空,因为即使调用了update方法,也是提交了才生效。
private boolean clearOnCommit;
//记录待更新的缓存
private final Map<Object, Object> entriesToAddOnCommit;
//记录未命中的缓存,与BlockingCache有关
private final Set<Object> entriesMissedInCache;
  1. 清空缓存 如果发生了更新,会调用clear方法,不会立即清空,而是提交时才会清空,因为即使调用了update方法,也是提交了才生效。
@Override
public void clear() {
  //clear被调用,也就是发生了update操作,不会立即失效,提交时才会执行真正的clear
  clearOnCommit = true;
  entriesToAddOnCommit.clear();
}
  1. 缓存读取
@Override
public Object getObject(Object key) {
  // 1. 从底层缓存读取数据
  Object object = delegate.getObject(key);
  
  // 2. 若未命中,记录key(用于BlockingCache释放锁)
  if (object == null) {
    entriesMissedInCache.add(key);
  }
  
  // 3. 若标记为"提交时清空缓存",也就是更新过,则直接返回 null(避免读取旧数据)
  if (clearOnCommit) {
    return null;
  } else {
    return object;
  }
}
  1. 缓存写入(putObject)

事务期间写入缓存的操作不会直接同步到底层缓存,而是暂存到 entriesToAddOnCommit,延迟生效,确保未提交事务的中间结果不会被其他事务读取,符合事务隔离性。

@Override
public void putObject(Object key, Object value) {
  // 暂存键值对,待事务提交时写入底层缓存
  entriesToAddOnCommit.put(key, value);
}
  1. 事务提交

大致流程:

  • 发生过更新(clear被调用过),则清空缓存
  • 提交暂存数据,释放查询为null的锁(与BlockingCache有关,后面再讲)
  • 状态重置
public void commit() {
  // 1. 若标记为"提交时清空",则先清空底层缓存
  if (clearOnCommit) {
    delegate.clear();
  }
  
  // 2. 将暂存的键值对写入底层缓存,并处理未命中的 key
  flushPendingEntries();
  
  // 3. 重置事务状态(清空暂存集合,重置标记)
  reset();
}

// 同步暂存的缓存操作到二级缓存
private void flushPendingEntries() {
  // a. 写入事务期间暂存的键值对
  for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
    delegate.putObject(entry.getKey(), entry.getValue());
  }
  
  // b. 处理未命中的key,释放锁
  for (Object key : entriesMissedInCache) {
    if (!entriesToAddOnCommit.containsKey(key)) {
      delegate.removeObject(key);
    }
  }
}

//状态重置
private void reset() {
  clearOnCommit = false;
  entriesToAddOnCommit.clear();
  entriesMissedInCache.clear();
}
  1. 事务回滚(rollback)

事务回滚时,丢弃暂存的缓存操作,并释放查询为null的锁:

public void rollback() {
  // 1. 移除未命中的 key(避免其他事务读到无效数据)
  unlockMissedEntries();
  
  // 2. 重置事务状态
  reset();
}

// 释放锁
private void unlockMissedEntries() {
  for (Object key : entriesMissedInCache) {
    delegate.removeObject(key);
  }
}

3. 缓存的构建

Mybatis的二级缓存是Mapper级别的,缓存对象是在解析Mapper配置时创建的,存储在Mybatis的Configuration中。

1、构造入口:Mapper 加载时的缓存初始化

MyBatis 启动时会解析所有 Mapper 接口(或 XML 文件),当检测到 Mapper 配置了二级缓存(如 <cache> 标签或 @CacheNamespace 注解)时,会触发 Cache 对象的构造,以 XML 解析为例:

// XMLMapperBuilder 解析 <cache> 标签的逻辑
private void cacheElement(XNode context) {
  if (context != null) {
    // 解析 cache 标签的属性(eviction、flushInterval、size、readOnly 等)
    String type = context.getStringAttribute("type", "PERPETUAL");
    Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
    String eviction = context.getStringAttribute("eviction", "LRU");
    Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
    Long flushInterval = context.getLongAttribute("flushInterval");
    Integer size = context.getIntAttribute("size");
    boolean readWrite = !context.getBooleanAttribute("readOnly", false);
    boolean blocking = context.getBooleanAttribute("blocking", false);
    Properties props = context.getChildrenAsProperties();
    
    // 构建 Cache 对象,并与当前 Mapper 命名空间绑定
    builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
  }
}

builderAssistant 是 MapperBuilderAssistant 的实例,它负责协调 Mapper 相关对象的创建,包括 Cache 对象。

2、Cache 对象的实际构造:CacheBuilder

MapperBuilderAssistant.useNewCache() 方法会委托 CacheBuilder 完成 Cache 对象的构建,这是 Cache 实例化的核心过程:

// MapperBuilderAssistant.useNewCache()
public Cache useNewCache(Class<? extends Cache> typeClass, Class<? extends Cache> evictionClass, Long flushInterval, Integer size, boolean readWrite, boolean blocking, Properties props) {
  // 1. 确定缓存实现类(默认 PerpetualCache)
  typeClass = valueOrDefault(typeClass, PerpetualCache.class);
  evictionClass = valueOrDefault(evictionClass, LruCache.class);
  
  // 2. 创建 CacheBuilder 并配置参数
  Cache cache = new CacheBuilder(currentNamespace) // 以 Mapper 命名空间为缓存 ID
      .implementation(typeClass)
      .addDecorator(evictionClass)
      .flushInterval(flushInterval)
      .size(size)
      .readWrite(readWrite)
      .blocking(blocking)
      .properties(props)
      .build(); // 触发 Cache 对象的构建
  
  // 3. 将构建好的 Cache 对象注册到 Configuration 中
  configuration.addCache(cache);
  
  // 4. 绑定当前 Mapper 命名空间与该 Cache 对象
  currentCache = cache;
  return cache;
}

经过上述流程后,就根据配置得到了一个完整的缓存装饰器链。CacheBuilder的构建过程:

    // CacheBuilder.java(缓存构建器)
    public Cache build() {
      // 设置默认实现(PerpetualCache)
      setDefaultImplementations();
      // 创建基础缓存(PerpetualCache)
      Cache cache = newBaseCacheInstance(implementation, id);
      // 注入属性(如size、flushInterval)
      setCacheProperties(cache);
      
      // 若不是自定义缓存,添加默认装饰器链
      if (PerpetualCache.class.equals(cache.getClass())) {
        for (Class<? extends Cache> decorator : decorators) {
          cache = newCacheDecoratorInstance(decorator, cache);
          setCacheProperties(cache);
        }
        // 应用标准装饰器(如LruCache、ScheduledCache等)
        cache = setStandardDecorators(cache);
      } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
        // 添加日志装饰器
        cache = new LoggingCache(cache);
      }
      return cache;
    }

    private Cache setStandardDecorators(Cache cache) {
      try {
        MetaObject metaCache = SystemMetaObject.forObject(cache);
        if (size != null && metaCache.hasSetter("size")) {
          metaCache.setValue("size", size);
        }
        // 如果设置了清空缓存的间隔时间,构造ScheduledCache装饰器
        if (clearInterval != null) {
          cache = new ScheduledCache(cache);
          ((ScheduledCache) cache).setClearInterval(clearInterval);
        }
        // 如果readOnly=false,构造SerializedCache装饰器
        if (readWrite) {
          cache = new SerializedCache(cache);
        }
        // 日志装饰器,打印debug日志,统计命中率
        cache = new LoggingCache(cache);
        // 同步装饰器
        cache = new SynchronizedCache(cache);
        // 阻塞装饰器
        if (blocking) {
          cache = new BlockingCache(cache);
        }
        return cache;
      } catch (Exception e) {
        throw new CacheException("Error building standard cache decorators.  Cause: " + e, e);
      }
}

缓存装饰器和作用:

装饰器类名核心功能关键字段 / 机制适用场景装饰器链位置(典型)
基础功能装饰器
PerpetualCache基础缓存实现,提供内存存储(非装饰器,是所有装饰器的底层实现)Map<Object, Object> cache(基于 HashMap 存储)作为二级缓存的基础存储载体最底层
SynchronizedCache为缓存操作添加同步锁,保证线程安全所有方法添加 synchronized 关键字多线程并发访问缓存场景靠近底层(如 PerpetualCache 之上)
缓存策略装饰器
LruCache实现 LRU(最近最少使用)淘汰策略,限制缓存大小LinkedHashMap<Object, Object> keyMap(按访问顺序排序),size(最大容量)缓存空间有限,需自动淘汰不常用数据基础缓存之上(如 PerpetualCache 外层)
FifoCache实现 FIFO(先进先出)淘汰策略Deque<Object> keyList(记录插入顺序),size(最大容量)数据按时间顺序失效的场景基础缓存之上
SoftCache使用软引用(SoftReference)存储缓存项,内存不足时自动回收Map<Object, SoftReference<Object>> cache允许缓存项在内存紧张时被回收,避免 OOM基础缓存之上
WeakCache使用弱引用(WeakReference)存储缓存项,GC 时自动回收Map<Object, WeakReference<Object>> cache缓存项生命周期与对象引用绑定,适合临时数据基础缓存之上
功能扩展装饰器
LoggingCache记录缓存命中率日志,方便调试和性能分析hitCount(命中次数)、requestCount(请求次数)需要监控缓存效果(命中率)的场景中层装饰(如策略装饰器之上)
SerializedCache对缓存值进行序列化 / 反序列化,实现对象深拷贝重写 putObject/getObject,使用 ObjectOutputStream/ObjectInputStreamreadOnly=false 时,保证缓存对象线程安全(避免修改共享实例)策略装饰器之上
BlockingCache缓存未命中时加锁,避免并发查询穿透到数据库ConcurrentHashMap<Object, CountDownLatch> locks(锁集合)高并发场景,防止缓存击穿(热点 key 失效时大量请求冲击数据库)靠近顶层(装饰器链外层)
ScheduledCache按固定时间间隔自动清空缓存long flushInterval(刷新间隔)、long lastFlushTime(上次刷新时间)数据定时更新,需定期失效的场景(如每小时刷新一次)基础缓存之上
3. Cache 对象的存储:Configuration -> MappedStatement

构建完成的 Cache 对象会被存储在 Configuration 类的 caches 集合中,这是 MyBatis 全局管理二级缓存的容器:

// Configuration 类中存储缓存的字段
protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");

// 添加缓存到注册表
public void addCache(Cache cache) {
  caches.put(cache.getId(), cache); // 以缓存 ID(即 Mapper 命名空间)为 key
}

// 获取缓存(后续执行 SQL 时会调用此方法)
public Cache getCache(String id) {
  return caches.get(id);
}

每个 Cache 对象的 id 就是对应的 Mapper 命名空间(如 com.example.mapper.UserMapper),这确保了二级缓存以 Mapper 为单位隔离。后续配置解析时,会进一步构建Mapper每个方法的MappedStatement,它封装了 Mapper 接口中单个方法(或XML中单个SQL标签)的所有配置信息。如果开启了缓存,MappedStatement会引用对应Cahce

总结

MyBatis二级缓存通过 “装饰器模式” 构建了灵活可扩展的缓存体系,设计很精巧,然而实践中使用的并不多:

  1. 分布式环境下的缓存共享难题,默认实现是单机模式,而现在多数是分布式环境,不同服务节点的缓存无法共享,缓存过期,不同的节点间也不会相互通知。当然,我们可以通过自定义缓存,实现Cache接口,借助Redis等来实现缓存,再配置中设置自己的实现类即可:<cache type="com.example.cache.RedisCache"/>。但依然难以解决缓存一致性问题。
  2. 缓存失效的方式过于粗暴,只要发生了修改,不管具体更新了多少数据,所有的缓存都会生效。没有自行管理缓存灵活。