mybatis · 2021-11-30 0

mybatis一级缓存与二级缓存

SqlSession和Executor以及Cache的关系

1.关系图

sqlsession

2.版本

版本:

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.2</version>
</dependency>

3.一二级区别

  1. 一级缓存: 基于PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该Session中的所有 Cache 就将清空

  2. 二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap 存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache

先查看二级缓存,然后查看一级缓存,再查询DB

4.CacheKey

MyBatis 认为的完全相同的查询,不是指使用 sqlSession 查询时传递给算起来 Session 的所有参数值完完全全相同,你只要保证statementId,rowBounds,最后生成的SQL语句,以及这个SQL语句所需要的参数完全一致就可以了

CacheKey 由以下条件决定:statementId + rowBounds + 传递给JDBC的SQL + 传递给JDBC的参数值

源码如下:

CachingExecutor 中的 Executor delegateSimpleExecutorCache cache 是二级缓存

TransactionalCacheManager tcm 用来管理二级缓存

  // CachingExecutor.java
  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    Cache cache = ms.getCache();
    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);
  }

  @Override
  public int update(MappedStatement ms, Object parameterObject) throws SQLException {
    flushCacheIfRequired(ms);
    return delegate.update(ms, parameterObject);
  }

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

PerpetualCache localCache 是一级缓存

  // BaseExecutor.java
  @Override
  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;
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } 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;
  }

  @Override
  public int update(MappedStatement ms, Object parameter) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    clearLocalCache();
    return doUpdate(ms, parameter);
  }

一级缓存

1.PerpetualCache

由于 MyBatis 使用 SqlSession 对象表示一次数据库的会话,对于会话级别的一级缓存是在 SqlSession 中控制的。

实际上, SqlSession 只是一个 MyBatis 对外的接口, SqlSession 将它的工作交给了 Executor 执行器这个角色来完成,负责完成对数据库的各种操作。当创建了一个 SqlSession 对象时,MyBatis 会为这个 SqlSession 对象创建一个新的Executor 执行器,而缓存信息就被维护在这个 Executor 执行器中,MyBatis 将缓存和对缓存相关的操作封装成了 Cache 接口中。

Session 级别的一级缓存实际上就是使用 PerpetualCache 维护的

源码如下:

// PerpetualCache.java
public class PerpetualCache implements Cache {

  private final String id;

  private Map<Object, Object> cache = new HashMap<>();
}

2.一级缓存失效

  1. 不同的SqlSession,使用不同的一级缓存
  2. 同一个方法,不同的参数,由于可能之前没有查询过,所以还会发新的sql
  3. 在这个sqlsssion期间执行上任何一次增删改操作,insert,update,delete操作会把缓存清空(把 PerpetualCacheMap<Object, Object> cache 清空)
  4. 手动清空缓存

3.禁用一级缓存

两种方式:

  1. 映射文件设置 flushCache="true"(默认<select>flushCache="false" ,非<select>flushCache="true",设置的是 MappedStatementflushCacheRequired),则查询之前先清空一级缓存,再查数据库 <select flushCache="true">

  2. 配置文件设置 local-cache-scope=statement(设置的是 ConfigurationlocalCacheScope) ,则查询之后即便放入了一级缓存,但存放完立马就给清了,下一次还是要查数据库 <setting name="localCacheScope" value="STATEMENT"/>

二级缓存

对于 SqlSession,当 commitclose 时,才把数据存入二级缓存

TransactionalCacheManager 管理二级缓存

getObject 方法从二级缓存中取数据

putObject 方法把数据存放到 TransactionalCacheMap<Object, Object> entriesToAddOnCommit

delegateSynchronizedCache,这是真正的二级缓存

执行 commit 方法时,才把数据存入二级缓存

源码如下:

  // TransactionalCache.java
  @Override
  public Object getObject(Object key) {
    // issue #116
    Object object = delegate.getObject(key);
    if (object == null) {
      entriesMissedInCache.add(key);
    }
    // issue #146
    if (clearOnCommit) {
      return null;
    } else {
      return object;
    }
  }

  @Override
  public void putObject(Object key, Object object) {
    entriesToAddOnCommit.put(key, object);
  }

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

  private void flushPendingEntries() {
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
      delegate.putObject(entry.getKey(), entry.getValue());
    }
    for (Object entry : entriesMissedInCache) {
      if (!entriesToAddOnCommit.containsKey(entry)) {
        delegate.putObject(entry, null);
      }
    }
  }

执行器 CachingExecutor,执行 commit方法

  // CachingExecutor.java
  @Override
  public void commit(boolean required) throws SQLException {
    delegate.commit(required);
    tcm.commit();
  }

commit 方法清除一级缓存

  // BaseExecutor.java
  @Override
  public void commit(boolean required) throws SQLException {
    if (closed) {
      throw new ExecutorException("Cannot commit, transaction is already closed");
    }
    clearLocalCache();
    flushStatements();
    if (required) {
      transaction.commit();
    }
  }

1.二级缓存配置

设置配置文件:

<!-- 二级缓存 默认是开启的 需要搭配映射文件的 cache 标签才能生效 -->
<setting name="cacheEnabled" value="true"/>

设置映射文件:

<cache/>

实体类需要实现序列化接口

public class Student implements Serializable

2.二级缓存的注意事项

  1. 缓存是以namespace为单位的,不同namespace下的操作互不影响
  2. insert,update,delete操作会清空所在namespace下的全部缓存(更新操作默认<select flushCache="true">
  3. 多表操作一定不要使用二级缓存,因为多表操作进行更新操作,一定会产生不正确的数据

3.禁用二级缓存

  1. 映射文件设置 <select flushCache="true">

  2. 映射文件设置 <select useCache="true">