Mybatis 一二級緩存實現原理與使用指南

Mybatis 與 Hibernate 一樣,支持一二級緩存。一級緩存指的是 Session 級別的緩存,即在一個會話中多次執行同一條 SQL 語句並且參數相同,則後面的查詢將不會發送到數據庫,直接從 Session 緩存中獲取。二級緩存,指的是 SessionFactory 級別的緩存,即不同的會話可以共享。

緩存,通常涉及到緩存的寫、讀、過期(更新緩存)等幾個方面,請帶着這些問題一起來探究Mybatis關於緩存的實現原理吧。

提出問題:緩存的查詢順序,是先查一級緩存還是二級緩存?

本文以 SQL 查詢與更新兩個流程來揭開 Mybatis 緩存實現的細節。

溫馨提示,建議在閱讀本文之前先閱讀筆者的另外幾篇文章:
1)源碼分析Mybatis MapperProxy初始化【圖文並茂】
2)源碼分析Mybatis MappedStatement的創建流程
3)【圖文並茂】源碼解析MyBatis Sharding-Jdbc SQL語句執行流程詳解
4)【圖文並茂】Mybatis執行SQL的4大基礎組件詳解



1、從 SQL 查詢流程看一二級緩存


溫馨提示,本文不會詳細介紹詳細的 SQL 執行流程,如果對其感興趣,可以查閱筆者的另外一篇文章:【圖文並茂】源碼解析MyBatis Sharding-Jdbc SQL語句執行流程詳解

1.1 創建Executor

具體實現由 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) {                                                           // @1
      executor = new CachingExecutor(executor);                 // @2
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

代碼@1:如果 cacheEnabled 爲 true,表示開啓緩存機制,緩存的實現類爲 CachingExecutor,這裏使用了經典的裝飾模式,處理了緩存的相關邏輯後,委託給的具體的 Executor 執行。

cacheEnable 在實際的使用中通過在 mybatis-config.xml 文件中指定,例如:

<configuration>
    <settings>
        <setting name="cacheEnabled" value="true">
    </settings>
</configuration>

該值默認爲true。

1.2 CachingExecutor#query

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);  // @1
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);   // @2
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);       // @3
}

代碼@1:根據參數生成SQL語句。

代碼@2:根據 MappedStatement、參數、分頁參數、SQL 生成緩存 Key。

代碼@3:調用6個參數的 query 方法。

緩存 Key 的創建比較簡單,本文就只貼出代碼,大家一目瞭然,大家重點關注組成緩存Key的要素。

BaseExecute#createCacheKey

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;
}

接下來重點看CachingExecutor的另外一個query方法。

CachingExecutor#query

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    Cache cache = ms.getCache();    // @1
    if (cache != null) {
      flushCacheIfRequired(ms);        // @2
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        List<E> list = (List<E>) tcm.getObject(cache, key);      // @3
        if (list == null) {                                                              // @4
          list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);    //@5
          tcm.putObject(cache, key, list); // issue #578 and #116                                                               // @6
        }
        return list;
      }
    }
    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);  //@7
}

代碼@1:獲取 MappedStatement 中的 Cache cache 屬性。

代碼@2:如果不爲空,則嘗試從緩存中獲取,否則直接委託給具體的執行器執行,例如 SimpleExecutor (@7)。

代碼@3:嘗試從緩存中根據緩存 Key 查找。

代碼@4:如果從緩存中獲取的值不爲空,則直接返回緩存中的值,否則先從數據庫查詢@5,將查詢結果更新到緩存中。

這裏的緩存即 MappedStatement 中的 Cache 對象是一級緩存還是二級緩存?通常在 ORM 類框架中,Session 級別的緩存爲一級緩存,即會話結束後就會失效,顯然這裏不會隨着 Session 的失效而失效,因爲 Cache 對象是存儲在於 MappedStatement 對象中的,每一個 MappedStatement 對象代表一個 Dao(Mapper) 中的一個方法,即代表一條對應的 SQL 語句,是一個全局的概念。

相信大家也會覺得,想繼續深入瞭解 CachingExecutor 中使用的 Cache 是一級緩存還是二級緩存,瞭解 Cache 對象的創建至關重要。關於 MappedStatement 的創建流程,建議查閱筆者的另外一篇博文:源碼分析Mybatis MappedStatement的創建流程。

本文只會關注 MappedStatement 對象流程中關於緩存相關的部分。

接下來將按照先二級緩存,再一級緩存的思路進行講解。

1.2.1 二級緩存

1.2.1.1 MappedStatement#cache屬性創建機制

從上面看,如果 cacheEnable 爲 true 並且 MappedStatement 對象的 cache 屬性不爲空,則能使用二級緩存。

我們可以看到 MappedStatement 對象的 cache 屬性賦值的地方爲:MapperBuilderAssistant 的 addMappedStatement 方法,從該方法的調用鏈可以得知是在解析 Mapper 定義的時候就會創建。

Mybatis 一二級緩存實現原理與使用指南
在這裏插入圖片描述

使用的 cache 屬性爲 MapperBuilderAssistant 的 currentCache,我們跟蹤一下該屬性的賦值方法:

public Cache useCacheRef(String namespace)

其調用鏈如下:
Mybatis 一二級緩存實現原理與使用指南
在這裏插入圖片描述

可以看出是在解析 cacheRef 標籤,即在解析 Mapper.xml 文件中的 cacheRef 標籤時,即二級緩存的使用和 cacheRef 標籤離不開關係,並且特別注意一點,其參數爲 namespace,即每一個 namespace 對應一個 Cache 對象,在 Mybatis 的方法中,通常namespace 對一個 Mapper.java 對象,對應對數據庫一張表的更新、新增操作。
public Cache useNewCache
其調用鏈如下圖所示:
Mybatis 一二級緩存實現原理與使用指南
在這裏插入圖片描述



在解析 Mapper.xml 文件中的 cache 標籤時被調用。

1.2.1.2 cache標籤解析

接下來我們根據 cache 標籤簡單看一下 cache 標籤的解析,下面以 xml 配置方式爲例展開,基於註解的解析,其原理類似,其代碼 XMLMapperBuilder 的 cacheElement 方法。

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);
      Properties props = context.getChildrenAsProperties();
      builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
  }

從上面 cache 標籤的核心屬性如下:

  • type
    緩存實現類,可選擇值:PERPETUAL、LRU 等,Mybatis 中所有的緩存實現類如下:
    Mybatis 一二級緩存實現原理與使用指南
    在這裏插入圖片描述


  • eviction
    移除算法,默認爲 LRU。
  • flushInterval
    緩存過期時間。
  • size
    緩存在內存中的緩存個數。
  • readOnly
    是否是隻讀。
  • blocking
    是否阻塞,具體實現請看 BlockingCache。

1.2.1.3 cacheRef

Mybatis 一二級緩存實現原理與使用指南
在這裏插入圖片描述

cacheRef 只有一個屬性,就是 namespace,就是引用其他 namespace 中的 cache。
Cache 的創建流程就講解到這裏,同一個 Namespace 只會定義一個 Cache。二級緩存的創建是在 *Mapper.xml 文件中使用了< cache/>、< cacheRef/>標籤時創建,並且會按 NameSpace 爲維度,爲各個 MapperStatement 傳入它所屬的 Namespace 的二級緩存對象。

二級緩存的查詢邏輯就介紹到這裏了,我們再次回看 CacheingExecutor 的查詢方法:
CachingExecutor#query

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    Cache cache = ms.getCache();    // @1
    if (cache != null) {
      flushCacheIfRequired(ms);        // @2
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        List<E> list = (List<E>) tcm.getObject(cache, key);      // @3
        if (list == null) {                                                              // @4
          list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);    //@5
          tcm.putObject(cache, key, list); // issue #578 and #116                                                               // @6
        }
        return list;
      }
    }
    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);  //@7
}

如果 MappedStatement 的 cache 屬性爲空,則直接調用內部的 Executor 的查詢方法。也就是如果在 *.Mapper.xm l文件中未定義< cache/>或< cacheRef/>,則 cache 屬性會爲空。

1.2.2 一級緩存

Mybatis 根據 SQL 的類型共有如下3種 Executor類型,分別是 SIMPLE, REUSE, BATCH,本文將以 SimpleExecutor爲 例來對一級緩存的介紹。

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());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {   // @1
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++;                                                              
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;     // @2
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);   // @3
      }
    } 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:queryStack:查詢棧,每次查詢之前,加一,查詢返回結果後減一,如果爲1,表示整個會會話中沒有執行的查詢語句,並根據 MappedStatement 是否需要執行清除緩存,如果是查詢類的請求,無需清除緩存,如果是更新類操作的MappedStatemt,每次執行之前都需要清除緩存。

代碼@2:如果緩存中存在,直接返回緩存中的數據。

代碼@3:如果緩存未命中,則調用 queryFromDatabase 從數據中查詢。

我們順便看一下 queryFromDatabase 方法,再來看一下一級緩存的實現類。

 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);   // @2
    } finally {
      localCache.removeObject(key);                                                            // @3
    }
    localCache.putObject(key, list);                                                              // @4
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

代碼@1:先往本地緩存存入一個特定值,表示正在執行中。

代碼@2:從數據中查詢數據。

代碼@3:先移除正在執行中的標記。

代碼@4:將數據庫中的值存儲到一級緩存中。

可以看出一級緩存的屬性爲 localCache,爲 Executor 的屬性。如果大家看過筆者發佈的這個 Mybatis 系列就能輕易得出一個結論,每一個 SQL 會話對應一個 SqlSession 對象,每一個 SqlSession 會對應一個 Executor 對象,故 Executor 級別的緩存即爲Session 級別的緩存,即爲 Mybatis 的一級緩存。

上面已經介紹了一二級緩存的查找與添加,在查詢的時候,首先查詢緩存,如果緩存未命中,則查詢數據庫,然後將查詢到的結果存入緩存中。

下面我們來簡單看看緩存的更新。

2、從SQL更新流程看一二級緩存


從更新的角度,更加的是關注緩存的更新,即當數據發生變化後,如果清除對應的緩存。

2.1 二級緩存

CachingExecutor#update

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

代碼@1:如果有必要則刷新緩存。

代碼@2:調用內部的 Executor,例如 SimpleExecutor。

接下來重點看一下 flushCacheIfRequired 方法。

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

TransactionalCacheManager#clear
public void clear(Cache cache) {
    getTransactionalCache(cache).clear();
}

TransactionalCacheManager 事務緩存管理器,其實就是對 MappedStatement 的 cache 屬性進行裝飾,最終調用的還是MappedStatement 的 getCache 方法得到其緩存對象然後調用 clear 方法,清空所有的緩存,即緩存的更新策略是隻要namespace 的任何一條插入或更新語句執行,整個 namespace 的緩存數據將全部清空。

2.2 一級緩存的更新

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);
}

其更新策略與二級緩存維護的一樣。

一二級緩存的的新增、查詢、更新就介紹到這裏了,接下來對其進行一個總結。

3、總結


3.1 一二級緩存作用序列圖

Mybatis 一二級緩存時序圖如下:
Mybatis 一二級緩存實現原理與使用指南
在這裏插入圖片描述

3.2 如何使用二級緩存

1、在mybatis-config.xml中將cacheEnable設置爲true。例如:

<configuration>
    <settings>
        <setting name="cacheEnabled" value="true">
    </settings>
</configuration>

不過該值默認爲true。

2、在需要緩存的表操作,對應的 Dao 的配置文件中,例如 *Mapper.xml 文件中使用 cache、或 cacheRef 標籤來定義緩存。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.winterchen.dao.UserDao" >
  <insert id="insert" parameterType="com.winterchen.model.UserDomain">
    //省略
  </insert>
  <select id="selectUsers" resultType="com.winterchen.model.UserDomain">
      //省略
  </select>
  <cache type="lru" readOnly="true" flushInterval="3600000"></cache>
</mapper>

這樣就定義了一個 Cache,其 namespace 爲 com.winterchen.dao.UserDao。其中 flushInterval 定義該 cache 定時清除的時間間隔,單位爲 ms。

如果一個表的更新操作、新增操作位於不同的 Mapper.xml 文件中,如果對一個表的操作的 Cache 定義在不同的文件,則緩存數據則會出現不一致的情況,因爲 Cache 的更新邏輯是,在一個 Namespace 中,如果有更新、插入語句的執行,則會清除該 namespace 對應的 cache 裏面的所有緩存。那怎麼來處理這種場景呢?cacheRef 閃亮登場。

如果一個 Mapper.xml 文件需要引入定義在別的 Mapper.xml 文件中定義的 cache,則使用 cacheRef,示例如下:

<cacheRef "namespace" = "com.winterchen.dao.UserDao"/>

一級緩存默認是開啓的,也無法關閉。

緩存的介紹就介紹到這裏。如果本文對您有所幫助,麻煩點一下贊,謝謝。


更多文章請關注微信公衆號:
Mybatis 一二級緩存實現原理與使用指南

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章