Mybatis的快取

吳楠予發表於2021-01-16

Mybatis的快取

mybatis是一個查詢資料庫的封裝框架,主要是封裝提供靈活的增刪改sql,開發中,service層能夠通過mybatis元件查詢和修改資料庫中表的資料;作為查詢工具,mybatis有使用快取,這裡講一下mybatis的快取相關原始碼。

快取

在計算機裡面,任何資訊都有源頭,快取一般指源頭資訊讀取後,放在記憶體或者其他讀取較快的地方,下次讀取相同資訊不去源頭查詢而是直接從記憶體(或者能快速存取的硬體)讀取。這樣可以減少硬體使用,提高讀取速度。

mybatis也是這樣,查詢資料庫的資料之後,mybatis可以把查詢結果快取到記憶體,下次查詢如果查詢語句相同,並且查詢相關的表的資料沒被修改過,就可以直接返回快取中的結果,而不用去查詢資料庫的語句,有效節省了時間。

關於mybatis中一級和二級快取命名

快取概念較早用於CPU讀取資料,有一級和二級快取,讀取順序是先一級快取,再二級快取。

按照這個概念,通過原始碼瞭解mybatis的Mapper中的快取是一級快取,SqlSession的中快取是二級快取。看到一些介紹mybatis快取的相關文章命名反過來的,稱SqlSession中的快取稱為一級快取,對此有疑惑...

簡單看一下mybatis快取相關原始碼

Mapper中的快取(一級)

mapper中的快取,預設配置是開啟,但需要在對映檔案mapper.xml中新增<cache/>標籤

<mapper namespace="userMapper">
	<cache/><!-- 新增cache標籤表示此mapper使用快取 -->
</mapper>

配置false可以關閉mapper中的快取

mybatis:
  configuration:
     cache-enabled: false #預設值為true,表示開啟

mapper快取的解析

org.apache.ibatis.builder.xml.XMLMapperBuilder

  private void configurationElement(XNode context) {
    try {
      //...
      cacheElement(context.evalNode("cache")); //解析mapper.xml中的cache標籤
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
  }

  private void cacheElement(XNode context) {
    if (context != null) { // if hava cache tag 如果有cache標籤才執行下面的邏輯
      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();
      builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);//建立mapper快取
    }
  }

org.apache.ibatis.builder.MapperBuilderAssistant.useNewCache():

  public Cache useNewCache(Class<? extends Cache> typeClass,
      Class<? extends Cache> evictionClass,
      Long flushInterval,
      Integer size,
      boolean readWrite,
      boolean blocking,
      Properties props) {
    Cache cache = new CacheBuilder(currentNamespace)
        .implementation(valueOrDefault(typeClass, PerpetualCache.class))
        .addDecorator(valueOrDefault(evictionClass, LruCache.class))
        .clearInterval(flushInterval)
        .size(size)
        .readWrite(readWrite)
        .blocking(blocking)
        .properties(props)
        .build();
    configuration.addCache(cache);//mapper快取賦值,如果cache標籤為空,不會執行此方法,currentCache為空
    currentCache = cache; 
    return cache;
  }

在對映檔案mapper中如果沒有cache標籤,解析時不會執行上面的useNewCache方法,cache為null,就不會使用mapper快取(相當於失效)。

查詢使用mapper快取邏輯

org.apache.ibatis.executor.CachingExecutor :

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    Cache cache = ms.getCache(); //獲取mapper快取
    if (cache != null) {//如果mapper快取物件不為空 嘗試在mapper快取中獲取(沒有cache標籤此物件就是空)
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        List<E> list = (List<E>) tcm.getObject(cache, key); //從mapper快取中獲取資料
        if (list == null) {
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); //如果為空,使用delegate查詢(BaseExecutor)
          tcm.putObject(cache, key, list); // 查詢結果儲存到mapper快取
        }
        return list;
      }
    }
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

上面程式碼中ms物件(MappedStatement )是在系統啟動時建立的物件,cache也是,不與SqlSession繫結,所以SqlSession不同,mapper快取依然可以使用,這區別於SqlSession中的快取。

SqlSession中的快取(二級)

通過檢視原始碼可知,SqlSsession中是有快取的,所以每次(新請求)會話SqlSession不同,快取是空的;相同的SqlSession中的快取才有效。

mybatis預設Sqlsession:org.apache.ibatis.session.defaults.DefaultSqlSession

構造方法中傳入executor(查詢執行物件)

  public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
    this.configuration = configuration;
    this.executor = executor;
    this.dirty = false;
    this.autoCommit = autoCommit;
  }

executor中攜帶二級快取成員變數:

  protected BaseExecutor(Configuration configuration, Transaction transaction) {
    this.transaction = transaction;
    this.deferredLoads = new ConcurrentLinkedQueue<>();
    this.localCache = new PerpetualCache("LocalCache"); //預設SqlSession中的快取
    this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
    this.closed = false;
    this.configuration = configuration;
    this.wrapper = this;
  }

查詢使用SqlSession快取邏輯

org.apache.ibatis.executor.BaseExecutor.query()

  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());
    
    List<E> list;
    try {
      queryStack++;
      	//localCache SqlSession中的快取
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
        //先從SqlSession中的快取中獲取,key是通過sql語句生成
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        // 如果快取中沒有 才從資料庫查詢
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    return list;
  }

  //從資料庫讀取資料
  private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      localCache.removeObject(key);//將SqlSession中的快取清除
    }
    localCache.putObject(key, list);//返回查詢結果之前,放入SqlSession中的快取 重新整理
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

二級快取和一級快取不用想,資料庫的資料被修改時要清空快取,不然資料有誤;至於怎麼清空,是另一套邏輯,mapper中的cache標籤可以配置一些引數,比如快取定期清空。

一級二級快取先後順序

從概念上來將,先讀取的就是一級快取,後讀取是二級快取,那麼Mapper中的快取是一級快取。

通過newExecutor原始碼可以知道這裡使用了類似裝飾者模式對executor進行了包裝,在建立Executor的時候,邏輯如下

org.apache.ibatis.session.Configuration.newExecutor()

  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);//建立CachingExecutor
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

  //CachingExecutor構造
  public CachingExecutor(Executor delegate) {
    this.delegate = delegate; //進行一層包裝 delegate = BaseExecutor
    delegate.setExecutorWrapper(this);
  }

開啟快取,查詢邏輯從SqlSession.selectList開始,先呼叫CachingExecutor物件

org.apache.ibatis.session.defaults.DefaultSqlSession.selectList()

  @Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
      //...
      //executor = CachingExecutor
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
  }

1 CachingExecutor.query 中嘗試讀取mapper中的快取(一級)

org.apache.ibatis.executor.CachingExecutor.query()

//1 Mapper cache
Cache cache = ms.getCache();
if (cache != null) {
      //...
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
            // delegate.query is BaseExecutor.query
        }
        return list;
      }
    }

2 BaseExecutor.query 中嘗試讀取SqlSession中的快取(二級)

org.apache.ibatis.executor.BaseExecutor.query()

//2 SqlSession cache
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
    handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
    //3 DabaBase
    list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

3 最後一級二級快取都沒有,就查詢資料庫

相關文章