本文采用知识共享 署名-相同方式共享 4.0 国际 许可协议进行许可。
访问 https://creativecommons.org/licenses/by-sa/4.0/ 查看该许可协议。

MyBatis 缓存

MyBatis 内置了两层缓存,SqlSession 级的一级缓存,和 Mapper 级的二级缓存,结构都为 HashMap

1) 一级缓存(LocalCache)

一级缓存存储位置 sqlSession -> executor(delegate) -> localCache

1.1) 一级缓存原理

当发起一次查询时,在 Executor 中会调用基础的 query 方法:

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
  BoundSql boundSql = ms.getBoundSql(parameter); // 获取实际执行的 SQL
  CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); // 1.1.1) 创建 CacheKey
  return query(ms, parameter, rowBounds, resultHandler, key, boundSql); // 1.1.2) 将 CacheKey 放入扩展的 query 方法查询
}

1.1.1) Create

CacheKey 由以下构成:

  1. Mapper namespace
  2. Statement id
  3. RowBounds 分页参数
  4. BoundSql 中的 SQL 执行参数列表
  5. Environment id
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
  if (closed) {
    throw new ExecutorException("Executor was closed.");
  }
  CacheKey cacheKey = new CacheKey();
  cacheKey.update(ms.getId());
  cacheKey.update(rowBounds.getOffset());
  cacheKey.update(rowBounds.getLimit());
  cacheKey.update(boundSql.getSql());
  List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
  TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
  // mimic DefaultParameterHandler logic
  for (ParameterMapping parameterMapping : parameterMappings) {
    if (parameterMapping.getMode() != ParameterMode.OUT) {
      Object value;
      String propertyName = parameterMapping.getProperty();
      if (boundSql.hasAdditionalParameter(propertyName)) {
        value = boundSql.getAdditionalParameter(propertyName);
      } else if (parameterObject == null) {
        value = null;
      } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
        value = parameterObject;
      } else {
        MetaObject metaObject = configuration.newMetaObject(parameterObject);
        value = metaObject.getValue(propertyName);
      }
      cacheKey.update(value);
    }
  }
  if (configuration.getEnvironment() != null) {
    // issue #176
    cacheKey.update(configuration.getEnvironment().getId());
  }
  return cacheKey;
}

1.1.2) Use

生成 CacheKey 后就轮到真正的查询了

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
  if (closed) {
    throw new ExecutorException("Executor was closed.");
  }
  if (queryStack == 0 && ms.isFlushCacheRequired()) {
    clearLocalCache();
  }
  List<E> list;
  try {
    queryStack++;
    list = resultHandler == null ? (List<E>) localCache.getObject(key) : null; // 如 CacheKey 已被缓存,直接取结果
    if (list != null) {
      handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
    } else {
      list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); // 无缓存,走 DB 查询,并将结果塞入 Cache
    }
  } finally {
    queryStack--;
  }
  if (queryStack == 0) {
    for (DeferredLoad deferredLoad : deferredLoads) {
      deferredLoad.load();
    }
    // issue #601
    deferredLoads.clear();
    if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
      // issue #482
      clearLocalCache();
    }
  }
  return list;
}

1.1.3) Clear

Executor 中还有个 clearLocalCache 方法用于清除一级缓存,那么何时会被清理呢:

  1. update(insert,update,delete) 必然
  2. query 配置了禁用缓存时
  3. commit 必然
  4. rollback 必然
  5. close 必然

2) 二级缓存(MethodCache)

二级缓存默认是不开启的,需要使用 <cache></cache>@CacheNamespace 标注 mapper 开启二级缓存
在每个 select statement 上还有两个参数可以控制:useCacheflushCache,注解开发时可使用 @Options 配置这两个参数

2.1) 二级缓存原理

2.1.1) Load

从 openSession 开始讲起,DefaultSqlSessionFactory:

public SqlSession openSession() {
  return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
  Transaction tx = null;
  try {
    final Environment environment = configuration.getEnvironment();
    final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
    tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
    final Executor executor = configuration.newExecutor(tx, execType); // 这里创建了一个 executor 实例
    return new DefaultSqlSession(configuration, executor, autoCommit); // 用 DefaultSqlSession 包装了 executor
  } catch (Exception e) {
    closeTransaction(tx); // may have fetched a connection so lets call close()
    throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}

获取 executor 实例的过程,如果配置文件中开启了二级缓存,则使用装饰者模式将默认 executor 包装扩展

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
  executorType = executorType == null ? defaultExecutorType : executorType;
  executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
  Executor executor;
  if (ExecutorType.BATCH == executorType) {
    executor = new BatchExecutor(this, transaction);
  } else if (ExecutorType.REUSE == executorType) {
    executor = new ReuseExecutor(this, transaction);
  } else {
    executor = new SimpleExecutor(this, transaction);
  }
  if (cacheEnabled) { // 是否开启二级缓存,如果开启则包装扩展
    executor = new CachingExecutor(executor);
  }
  executor = (Executor) interceptorChain.pluginAll(executor);
  return executor;
}

从 CachingExecutor 中成员变量和构造方法可以看出装饰者模式

public class CachingExecutor implements Executor {

  private final Executor delegate;
  private final TransactionalCacheManager tcm = new TransactionalCacheManager();

  public CachingExecutor(Executor delegate) {
    this.delegate = delegate;
    delegate.setExecutorWrapper(this);
  }
}

CachingExecutor 中真正的查询是这个方法

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
    throws SQLException {
  Cache cache = ms.getCache(); // 从 MappedStatement 加载 Cache(此 Cache 是在加载配置时初始化的)
  if (cache != null) {
    flushCacheIfRequired(ms);
    if (ms.isUseCache() && resultHandler == null) {
      ensureNoOutParams(ms, boundSql);
      @SuppressWarnings("unchecked")
      List<E> list = (List<E>) tcm.getObject(cache, key);
      if (list == null) {
        list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
        tcm.putObject(cache, key, list); // issue #578 and #116
      }
      return list;
    }
  }
  return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

以上就是二级缓存的加载。

2.1.2) Create

创建就是在上一节 tcm.putObject(cache, key, list) 这一行,数据结构和一级缓存基本一致

private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();

public void putObject(Cache cache, CacheKey key, Object value) {
  getTransactionalCache(cache).putObject(key, value);
}

2.1.3) Use

使用和还是在这个核心 query 方法中,但是这个 putObject 只是将数据放入了一个待提交的 Map 中
,在 commit 方法被调用时才真正提交至二级缓存,略复杂先不深究

List<E> list = (List<E>) tcm.getObject(cache, key); // 从二级缓存中取
if (list == null) { // 取不到则走 DB 查询并塞入缓存
  list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;

2.1.4) Clear

CachingExecutor 的清除缓存方法为 flushCacheIfRequired 方法用于操作 clear 的标志位

private void flushCacheIfRequired(MappedStatement ms) {
  Cache cache = ms.getCache();
  if (cache != null && ms.isFlushCacheRequired()) {
    tcm.clear(cache);
  }
}
public void clear() {
  clearOnCommit = true;
  entriesToAddOnCommit.clear();
}

真正的清理缓存也是在 TransactionCache 的 commit 中进行的

public void commit() {
  if (clearOnCommit) {
    delegate.clear();
  }
  flushPendingEntries();
  reset();
}

二级缓存略微复杂,先暂搁了,本文也参考的美团的一篇文章:
聊聊 MyBatis 缓存机制