天天看點

Mybatis二級緩存原了解析及如何第三方擴充二級緩存

作者:王朋code

在《Mybatis一級緩存原了解析》一文中我們知道了Mybatis的一級緩存是在BaseExecutor執行器中(如果隻使用一級緩存,那麼隻需要建立SimpleExecutor即可,因為SimpleExecutor繼承BaseExecutor),也提到了二級緩存在CachingExecutor中。但是在Mybatis建立執行器的過程中(newExecutor)為何要通過裝飾器設計模式建立執行器CachingExecutor呢?

隻想看如何使用,則可以直接看總結!

我們先回顧一下這段代碼:

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    Executor executor;
    //然後就是簡單的3個分支,産生3種執行器BatchExecutor/ReuseExecutor/SimpleExecutor
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      //我們知道上面傳的是SIMPLE,是以會建立SimpleExecutor,SimpleExecutor繼承BaseExecutor
      executor = new SimpleExecutor(this, transaction);
    }
  	//cacheEnabled預設就是true,是以必然傳回的就是CachingExecutor
    //如果要求緩存,生成另一種CachingExecutor(預設就是有緩存),裝飾者模式,是以預設都是傳回CachingExecutor
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    //此處調用插件,通過插件可以改變Executor行為,pageHepler就是在這裡增加了插件
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }           

通過上面的我們可以看到,如果不使用二級緩存,可以将cacheEnabled這個boolean類型字段置為false(預設是true):

<setting name="cacheEnabled" value="false"/>           

那麼既然預設為true,那麼它所有的查詢都會走二級緩存嗎?

帶着這個問題,我們來研究一下Mybatis的二級緩存:

我們先回到在DefaultSqlSession的查詢方法中

public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      //根據statement id找到對應的MappedStatement
      MappedStatement ms = configuration.getMappedStatement(statement);
      //轉而用執行器來查詢結果,注意這裡傳入的ResultHandler是null
      //這裡我們可以看到使用的是executor,這裡的executor我們需要重點看一下,這裡是CachingExecutor!!!
      //我們來看這行代碼
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }           

然後我們才看CachingExecutor的query方法:

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    //這裡會先判斷目前的MappedStatement有沒有cache,如果沒有,則會直接查詢一級緩存
    //這裡的緩存擷取看下面的代碼即可
    Cache cache = ms.getCache();
    //預設情況下是沒有開啟緩存的(二級緩存).要開啟二級緩存,你需要在你的 SQL 映射檔案中添加一行: <cache/>
    //簡單的說,就是先查CacheKey,查不到再委托給實際的執行器去查
    if (cache != null) {
      //這裡對應的Statement中的flushCache="true",即在執行某個sql前是否重新整理緩存
      flushCacheIfRequired(ms);
      //UseCache對應select标簽中的useCache="true",查詢結果是否進行緩存
      if (ms.isUseCache() && resultHandler == null) {
        //開始從二級緩存中查詢資料
        List<E> list = (List<E>) tcm.getObject(cache, key);
        //沒有資料則查詢SimpleExecutor
        if (list == null) {
          //我們知道delegate=SimpleExecutor,而SimpleExecutor繼承了BaseExecutor
          //這裡實際就是調用的BaseExecutor的query方法!!!
          list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          //存入二級緩存
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    //如果上面的二級緩存沒有查到資料,則會執行SimpleExecutor的query方法,由于一級緩存預設開啟
     //會先去查一級緩存,若沒有資料,則會查詢資料庫
    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }           

通過上面的代碼我們知道了Cache cache = ms.getCache();,如果目前的cache為null,則直接查詢SimpleExecutor,這裡說明如果我們沒有配置Cache,則不會走二級緩存。

那麼Cache是什麼呢?

<cache type="org.apache.test.BusinessRequestMonitorLogCache"/>           

調用鍊:parse->parseConfiguration->mapperElement->parse->configurationElement->cacheElement()

篇幅問題,我們直接看:XMLMapperBuilder#cacheElement()

//這裡是解析上面的xml中的cache标簽
private void cacheElement(XNode context) throws Exception {
    if (context != null) {
      //這裡對應的二級緩存的類型
      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);
      //讀入額外的配置資訊,易于第三方的緩存擴充,例:
//    <cache type="com.domain.something.MyCustomCache">
//      <property name="cacheFile" value="/tmp/my-custom-cache.tmp"/>
//    </cache>
      Properties props = context.getChildrenAsProperties();
      //調用builderAssistant.useNewCache
      builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }           
public Cache useNewCache(Class<? extends Cache> typeClass,
      Class<? extends Cache> evictionClass,
      Long flushInterval,
      Integer size,
      boolean readWrite,
      boolean blocking,
      Properties props) {
   //這裡面又判斷了一下是否為null就用預設值,有點和XMLMapperBuilder.cacheElement邏輯重複了
  //這裡如果類型為空,則使用預設的二級緩存PerpetualCache  
  typeClass = valueOrDefault(typeClass, PerpetualCache.class);
    evictionClass = valueOrDefault(evictionClass, LruCache.class);
    //調用CacheBuilder建構cache,id=currentNamespace
    Cache cache = new CacheBuilder(currentNamespace)
        .implementation(typeClass)
        .addDecorator(evictionClass)
        .clearInterval(flushInterval)
        .size(size)
        .readWrite(readWrite)
        .blocking(blocking)
        .properties(props)
        .build();
    //加入緩存
    configuration.addCache(cache);
    //目前的緩存
    currentCache = cache;
    return cache;
  }           

通過上面的代碼把讀取到的緩存對象指派給currentCache,這個currentCache是Mapper級别的,而Mybatis在解析每一個Statement标簽時通過MapperBuilderAssistant#addMappedStatement方法将currentCache指派給了每一個Statement标簽

setStatementCache(isSelect, flushCache, useCache, currentCache, statementBuilder);

private void setStatementCache(
  //是否是查詢語句
      boolean isSelect,
  //是否重新整理緩存
      boolean flushCache,
  //是否使用緩存
      boolean useCache,
  //緩存資訊
      Cache cache,
      MappedStatement.Builder statementBuilder) {
    flushCache = valueOrDefault(flushCache, !isSelect);
    useCache = valueOrDefault(useCache, isSelect);
    statementBuilder.flushCacheRequired(flushCache);
    statementBuilder.useCache(useCache);
    statementBuilder.cache(cache);
  }           

通過上面的代碼,Mybatis将我們設定的<cache type="org.apache.test.BusinessRequestMonitorLogCache"/>指派給每一個Statement标簽。

我們來簡單總結一下今天的内容:

Mybatis預設使用的執行器就是二級緩存執行器,不想使用可以關閉:

<setting name="cacheEnabled" value="false"/>           

即便我們不關閉,Mybatis執行sql依然使用的是二級緩存執行器,但每次查詢并不會使用二級緩存,想要使用二級緩存,我們需要先在Mapper.xml中配置:

<cache type="org.apache.test.BusinessRequestMonitorLogCache"/>           

如果不設定type标簽,則使用預設的二級緩存:PerpetualCache,不知道大家對這個名字熟悉不,這裡使用的是跟一級緩存一樣的本地HashMap

如果設定了type标簽,則會使用我們自定義的緩存,如果我們想內建第三方緩存配件(如redis),則可以像我一樣,建立我們自定義緩存類,實作Cache接口,重寫對應的方法即可!

即便配置了Mapper級别的緩存,每一個查詢Statement依然不會走二級緩存,需要在對應的select标簽中加上useCache="true"。

如果想執行某個sql前清除緩存,則可以在對應的标簽中加上flushCache="true"

通過以上的配置,在對應的select查詢時會調用二級緩存!

需要注意的是:二級緩存是事務性的(關于二級緩存事務,由于篇幅,這裡隻做簡單了解)。這意味着,當 SqlSession 完成并送出時,或是完成并復原,但沒有執行 flushCache=true 的 insert/delete/update 語句時,緩存會獲得更新。

如果設定了事務手動送出,則事務不送出,二級緩存不存在

為什麼事務不送出,二級緩存不生效?因為二級緩存使用TransactionalCacheManager來管理,當進行putObject即存入二級緩存時,隻是添加到了entriesToAddOnCommit (也是個map,可以了解為臨時存放)裡面,隻有它的commit()方法被調用的時候才會調用flushPendingEntries()真正寫入到我們的二級緩存中。