Mybatis源碼系列5二級緩存

NO IMAGE

2020年的Flag已經立完,不知道靠不靠譜,但是擋不住對未來美好的嚮往

啟用二級緩存二級緩存的位置二級緩存的樣子二級緩存的工作原理裝飾器模式事務型預緩存二級緩存的刷新總結:

Mybatis 的二級緩存相比一級緩存就複雜的多了,如果用一句話來說明Mybatis的二級緩存:

二級緩存是一個全局性,事務性,多樣性的緩存

那問題來了:

二級緩存在哪裡?

二級緩存長什麼樣子?

全局性,事務性,多樣性如何體現?

工作原理是怎麼樣的呢?

來一探究竟

啟用二級緩存

分為三步走:

1)開啟全局二級緩存配置:
2) 在需要使用二級緩存的Mapper配置文件中配置二級緩存類型

  • 為每一個Mapper分配一個Cache緩存對象(使用節點配置)
  • 多個Mapper共用一個Cache緩存對象(使用節點配置);

3)在具體CURD標籤上配置 useCache=true

二級緩存的位置

上文開啟二級緩存步驟中,可以看出,二級緩存的配置是在xml文件中。所以想要探究二級緩存在哪裡。還是得從xml文件的解析過程入手。

在[xml文件的解析]()一文講過,Mapper配置文件的解析是由XMLMapperBuilder 解析器解析的

//-----------XMLMapperBuilder類
private void configurationElement(XNode context) {
    try {
      ...
      //解析cache標籤
      cacheRefElement(context.evalNode("cache-ref"));
      cacheElement(context.evalNode("cache"));
      ...
    } catch (Exception e) {
    }
}
//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);
      Properties props = context.getChildrenAsProperties();
        //構建助手幫助創建Cache
      builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
  }
//-----------MapperBuilderAssistant類
public Cache useNewCache(...) {
    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);//添加到configuration一份
    currentCache = cache;//設置到當前臨時變量
    return cache;
  }
public MappedStatement addMappedStatement(....){
    MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType).....cache(currentCache);//設置臨時緩存變量
    MappedStatement statement = statementBuilder.build();//創建MappedStatement
    configuration.addMappedStatement(statement);
    return statement;
}

可以看出:

  • 二級緩存就是每一個SQL模板MappedStatement 實例的cache屬性,他部署
  • 同一個namespace下的所有MappedStatement.cache屬性 指向同一個cache對象。共用一個二級緩存

二級緩存的樣子

二級緩存具有多樣性,我們可以根據需求配置不同類型的二級緩存。
有哪些呢?

大類類型緩存名稱描述
基礎實現基礎類PerpetualCache基礎緩存,本質是包裝了HashMap
裝飾類算法類FifoCache先進先出緩存
LruCache最近最少使用
引用類SoftCache軟引用,內存不夠發生GC時刪除
WeakCache弱引用,發生GC就回收
技術增強類SerializedCache將緩存對象在保存前序列化和獲取後反序列化
SynchronizedCache對緩存的所有方法都加上synchronized
業務增強類LoggingCache記錄緩存的日誌,比如什麼時候進來的,什麼時候被刪除的
TransactionalCache事務性緩存
ScheduledCache定期刪除緩存
BlockingCache緩存阻塞

可以看出二級緩存的種類很多。mybatis是如何組織二級緩存的呢?

重點就在參數配置上,

參數描述
type緩存底層實現,默認是PerpetualCache
eviction清除策略 LRU、FIFO
flushInterval刷新間隔,ScheduledCache
size緩存的大小
readWrite緩存的讀寫
blocking當緩存key不存在時,是否直接查詢數據庫。默認false

參數的不同直接影響了二級緩存的樣子

//根據屬性配置來構建cache
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();
  //build方法
 public Cache build() {
    setDefaultImplementations();
    Cache cache = newBaseCacheInstance(implementation, id);
    setCacheProperties(cache);

    if (PerpetualCache.class.equals(cache.getClass())) {
      for (Class<? extends Cache> decorator : decorators) {
        cache = newCacheDecoratorInstance(decorator, cache);
        setCacheProperties(cache);
      }
      cache = setStandardDecorators(cache);
    } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
      cache = new LoggingCache(cache);
    }
    return cache;
  }
//一個標準的二級緩存應該是這樣的。
private Cache setStandardDecorators(Cache cache) {
    try {
      MetaObject metaCache = SystemMetaObject.forObject(cache);
      if (size != null && metaCache.hasSetter("size")) {
        metaCache.setValue("size", size);
      }
      if (clearInterval != null) {
      //如果配置了清理時間,使用ScheduledCache裝飾
        cache = new ScheduledCache(cache);
        ((ScheduledCache) cache).setClearInterval(clearInterval);
      }
      if (readWrite) {
      //如果配置了讀寫,使用SerializedCache裝飾
        cache = new SerializedCache(cache);
      }
      //使用LoggingCache裝飾
      cache = new LoggingCache(cache);
      //使用SynchronizedCache 裝飾
      cache = new SynchronizedCache(cache);
      if (blocking) {
      //如果配置阻塞,使用BlockingCache裝飾
        cache = new BlockingCache(cache);
      }
      return cache;
    } catch (Exception e) {
      throw new CacheException("Error building standard cache decorators.  Cause: " + e, e);
    }
  }

二級緩存採用裝飾器模式來設計。通過不同的配置,使用不同功能的緩存裝飾器來裝飾基礎緩存,使基礎緩存具有特殊的功能。

也就是說:
二級緩存= 多級裝飾器+ 基礎緩存類

二級緩存的工作原理

說到二級緩存的工作原理,可以用兩個知識點來總結

  • 裝飾器
  • 事務型預緩存

裝飾器模式

CachingExecutor
在創建DefaultSqlSession的執行器Executor時,如果開啟了二級緩存功能,會創建一個裝飾器CachingExecutor,來裝飾基礎Executor。

 public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    ...
    if (cacheEnabled) {
    //二級緩存裝飾器
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;

  }

CachingExecutor 執行器內部創建一個TransactionalCacheManager 事務緩存管理,並使用delegate 指向基礎Executor

public class CachingExecutor implements Executor {

 //目標Executor
  private Executor delegate;
  private TransactionalCacheManager tcm = new TransactionalCacheManager();

  public CachingExecutor(Executor delegate) {
    this.delegate = delegate;
    delegate.setExecutorWrapper(this);
  }
}

當開啟二級緩存的情況下執行sqlsession的select方法時,首先會執行CachingExecutor的query方法。

public <E> List<E> query(...) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(...);
  }
 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, parameterObject, boundSql);
           //先查詢二級緩存
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
        //二級緩存沒有,交給基礎執行器Executor 去執行查詢操作
          list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          //放入預緩存中。
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

流程:

  • 先獲取二級緩存。
  • 判斷當前SQL是否開啟二級緩存。
  • 開啟的情況下,先去二級緩存查詢。
  • 二級緩存有,直接返回
  • 二級緩存沒有,交給基礎執行器,走一級緩存執行過程。並把直接結果放入事務預緩存區。

事務型預緩存

在二級沒有數據的情況下,通過BaseExecutor從數據庫中查詢到結果後,並沒有直接放入二級緩存。而是先放入的事務預緩存中。

tcm.putObject(cache, key, list);

來看看這個預緩存區,如何工作。

//事務緩存管理者
public class TransactionalCacheManager {
private Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
public void putObject(Cache cache, CacheKey key, Object value) {
    getTransactionalCache(cache).putObject(key, value);
}
private TransactionalCache getTransactionalCache(Cache cache) {
    TransactionalCache txCache = transactionalCaches.get(cache);
    if (txCache == null) {
      txCache = new TransactionalCache(cache);
      transactionalCaches.put(cache, txCache);
    }
    return txCache;
  }
}
//事務預緩存
public class TransactionalCache implements Cache {
  private Cache delegate;
  private boolean clearOnCommit;
  private Map<Object, Object> entriesToAddOnCommit;
  private Set<Object> entriesMissedInCache;
  public void putObject(Object key, Object object) {
    entriesToAddOnCommit.put(key, object);
  }
}

事務型緩存TransactionalCache,也可以理解為預緩存區,是通過裝飾器模式設計的預緩存,通過delegate屬性指向二級緩存,他使得二級緩存具有事務特性。

TransactionalCache 由TransactionalCacheManager事務緩存管理者,進行統一管理。

工作原理:

List<E> list = (List<E>) tcm.getObject(cache, key);
//獲取當前key在二級緩存是否對應數據
 public Object getObject(Cache cache, CacheKey key) {
    return getTransactionalCache(cache).getObject(key);
  }
  //獲取事務預緩存。
 private TransactionalCache getTransactionalCache(Cache cache) {
    TransactionalCache txCache = transactionalCaches.get(cache);
    if (txCache == null) {
      txCache = new TransactionalCache(cache);
      transactionalCaches.put(cache, txCache);
    }
    return txCache;
  }

查詢過程

  • 通過TransactionalCacheManager 查詢二級緩存中是否由當前key對應的緩存數據。
  • TransactionalCacheManager 首先會檢查當前當前二級緩存是否被事務緩存TransactionalCache裝飾,如果沒有裝飾,就創建一個TransactionalCache裝飾一下二級緩存。
  • TransactionalCache#getObject(Key)方法會去二級緩存中查詢。

緩存過程:

//TransactionalCacheManager
public void commit() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.commit();
    }
  }
//TransactionalCache
public void commit() {
    if (clearOnCommit) {
      delegate.clear();
    }
    flushPendingEntries();//刷到二級緩存中
    reset();//清空預緩存
  }
  • 通過TransactionalCacheManager#putObject 方法把從數據查詢的結果放入TransactionalCache預緩存中。
  • 當Sqlsession執行commit時,執行TransactionalCacheManager#commit方法,把當前預緩存中的數據正式提交到二級緩存中。並清空預緩存區。

小結:

二級緩存的工作原理: 一個緩存執行器 + 一個預緩存 + 二級緩存

二級緩存的刷新

insert、update、delete操作後都會引發二級緩存的刷新

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);//清空二級緩存
    }
  }

總結:

在二級緩存的設計上,MyBatis大量地運用了裝飾者模式,如CachingExecutor, 以及各種Cache接口的裝飾器。

  • 二級緩存實現了Sqlsession之間的緩存數據共享,屬於namespace級別
  • 二級緩存具有豐富的緩存策略。
  • 二級緩存可由多個裝飾器,與基礎緩存組合而成
  • 二級緩存工作由 一個緩存裝飾執行器CachingExecutor和 一個事務型預緩存TransactionalCache 完成。

如果本文任何錯誤,請批評指教,不勝感激 !
如果覺得文章不錯,點個贊

微信公眾號:源碼行動
享學源碼,行動起來,來源碼行動

Mybatis源碼系列5二級緩存

相關文章

面試官刁難:Java字符串可以引用傳遞嗎?

Filecoin規範②時空證明

APP網絡優化之DNS優化實踐

窺探原理:手寫一個JavaScript打包器