Mybatis源碼閱讀之--本地(一級)緩存實現原理分析


前言:
Mybatis為了提升性能,內置了本地緩存(也可以稱之為一級緩存),在mybatis-config.xml中可以設置localCacheScope中可以配置本地緩存的作用域,包含兩個值session和statement,其中session選項表示本地緩存在整個session都有效,而statement只能在一條語句中有效(這條語句有嵌套查詢--nested query/select)。
下面分析一下mybatis本地緩存的實現原理。

本地緩存是在Executor內部構建,Executor包含了三個實現類,SimpleExecutor,BatchExecutor以及CachingExecutor,其中CachingExecutor是開啟了二級緩存才會用到的,這里主要是SimpleExecutor和BatchExecutor,他們都實現了BaseExecutor,而BaseExecutor中正是進行了一級緩存的處理。

public abstract class BaseExecutor implements Executor {
  protected PerpetualCache localCache; // 一級緩存,實質就是一個HashMap<Object, Object>
  protected PerpetualCache localOutputParameterCache; // 出參一級緩存,當statment為callable的時候使用
}

在BaseExecutor中定義了一個PerpetualCache類型的localCache屬性,用來保存一級緩存
而PerpetualCache類的主要功能如下:

public class PerpetualCache implements Cache {
  private final String id; // 該緩存的id
  private final Map<Object, Object> cache = new HashMap<>();
  // ...其他一些獲取緩存數據、移除緩存數據的方法
}

其中包含了兩個屬性,id表示緩存的唯一標識,cache是一個HashMap類型的對象,里面存放所有已經緩存的數據
也就是是說Mybatis的一級緩存實質就是一個HashMap。

再回過頭看一看BaseExecutor中的一級緩存處理過程(下述中的代碼片段都是BaseExecutor類中的,不會再把類加上了):

  1. select添加緩存
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    // 獲得緩存鍵
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    // 根據cachekey執行查詢
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
  }

首先創建緩存鍵key,然后根據key再查詢。
下面代碼展示了根據key進行查詢的邏輯

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

先看第二個if--如果當前的查詢語句設置了清除緩存的屬性為true,那么就要把一級緩存清除
當然里面還需要滿足queryStack==0的條件,這個條件涉及到了嵌套查詢(nested select/query),如果是嵌套查詢的最外層查詢(第一個查詢),才進行緩存的清理動作,否則不進行。這里的queryStack是查詢的層級,取決於nested select的層數,例如一個Blog有一個Author,一個Author有一個Account,其中Author和Account都使用了嵌套查詢,並且不是延遲加載(fetchType設置),那么Author查詢的時候queryStack就會是1,Account查詢的時候queryStack為2。針對嵌套查詢這里就說這么多,后續會專門寫一篇嵌套查詢原理的文章,包括非延遲加載以及延遲加載的不同情況的處理方式。

    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }

下面代碼片段展示了從緩存中取數據的邏輯

  list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
``
直接調用localCache的getObject方法,但是需要在resultHandler不為null的情況,因為如果查詢數據是傳入了ResultHandler,那么會返回null,數據由ResultHandler進行處理。
如果緩存中查到了數據,那么會處理緩存的出參(出參只有在MappedStatement類型為Callable時才會有,其他的STATEMENT/PREPAREDSTATMENT都沒有)
如果沒有查到數據,那么從數據庫中查詢
```java
    list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);

數據查詢完了之后執行queryStack--操作。
進入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);
    } finally {
      // 刪除占位
      localCache.removeObject(key);
    }
    // 將數據放入到緩存中
    localCache.putObject(key, list);
    // 對於callable的statement來說,出參也需要緩存,而出參也是放在了入參中
    // 因此這里緩存了入參
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

主要步驟:

  1. 先在一級緩存中設置一個占位符,EXECUTION_PLACEHOLDER,
    此處代碼的作用就是為了防止嵌套查詢是查詢了相同的數據
    舉個例子,一個Blog有一個Author,而Author中又嵌套了一個Blog,那么Blog還沒有放到緩存中,但是嵌套查詢現在查Author,Author中的Blog又是第一個Blog查詢的數據,這里放置一個占位符就是為了說明,這個Blog已經在查詢了,結果還沒出來而已,不要急,等結果出來了再進行配對。
  2. 執行子類的doQuery方法,查詢數據
  3. 刪除緩存占位、將查詢出的數據放入到緩存中。
  4. 如果此查詢語句是CALLABLE類型的,那么要把出參也緩存
    以上四部做完之后從數據庫中查詢數據就結束了,其中第一步可能有些人還是很困惑,大家可以執行一些測試看一看。

再次將思路返回到query方法中,

    if (queryStack == 0) { // 最外層的查詢已經結束
      // 所有非延遲的嵌套查詢也已經查完了,那么就可以把嵌套查詢的結果放入到需要的對象中
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }

當最外層查詢結束時,需要執行一些清理動作:

  1. 執行所有嵌套查詢的連接操作,上面例子中的Blog->Author->Blog,會把author中的Blog設置正確
  2. 清除嵌套查詢
  3. 如果當前語句的一級緩存作用域是statement的話,要把一級緩存清空
    上面的第一步和第二部需要結合ResultSetHandler共同分析,后面分析嵌套查詢的時候再做詳細的介紹,這里大家心中有個了解即可。
    至此,查詢過程的緩存處理就已經結束了
    下面簡單看一下cleanLocalCache方法
  public void clearLocalCache() {
    if (!closed) {
      localCache.clear();
      localOutputParameterCache.clear();
    }
  }

也很簡單,就是把localCache和localOutputParameterCache置空。

接下來就分析update(其中insert/update/delete都統稱為update)時,一級緩存如何處理:

  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();
    // 使用子類的doUpdate方法
    return doUpdate(ms, parameter);
  }
``
先把緩存清空,然后調用子類的doUpdate執行具體的更新操作

另外事務的提交以及回滾都會清空以及緩存,代碼如下:
```java
  public void commit(boolean required) throws SQLException {
    if (closed) {
      throw new ExecutorException("Cannot commit, transaction is already closed");
    }
    clearLocalCache(); // 清除緩存
    flushStatements();
    if (required) {
      transaction.commit();
    }
  }
  public void commit(boolean required) throws SQLException {
    if (closed) {
      throw new ExecutorException("Cannot commit, transaction is already closed");
    }
    clearLocalCache(); // 清理緩存
    flushStatements();
    if (required) {
      transaction.commit();
    }
  }
  public void rollback(boolean required) throws SQLException {
    if (!closed) {
      try {
        clearLocalCache(); // 清理緩存
        flushStatements(true);
      } finally {
        if (required) {
          transaction.rollback();
        }
      }
    }
  }

因此,在一個sqlSession執行了commit或者rollback方法后,一級緩存已經沒有了數據,如果再次執行相同的查詢操作,那么會重新從數據庫中查詢。
一級緩存需要注意的事項:
在實際開發中,有可能對查詢數據進行一些操作,比如修改一些字段,或者一個列表中刪除/添加一些數據,再次執行相同的查詢,返回的不會是數據庫中的數據,而是經過修改的數據,因此最好不要對Mybatis返回的數據進行修改操作。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM