Mybatis源碼系列5-二級緩存

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

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

Mybatis 的二級緩存相比一級緩存就複雜的多了,若是用一句話來講明Mybatis的二級緩存:算法

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

那問題來了:數據庫

二級緩存在哪裏?緩存

二級緩存長什麼樣子?微信

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

工做原理是怎麼樣的呢?mybatis

來一探究竟app

啓用二級緩存

分爲三步走:

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 完成。

若是本文任何錯誤,請批評指教,不勝感激 !
若是以爲文章不錯,點個贊

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

相關文章
相關標籤/搜索