從源碼層面談談mybatis的緩存設計

從源碼聊聊mybatis一次查詢都經歷了些什麼一文中咱們梳理了mybatis執行查詢SQL的具體流程,在Executor中簡單提到了緩存。本文將從源碼一步一步詳細解析mybatis緩存的架構,以及自定義緩存等相關內容。因爲一級緩存是寫死在代碼裏面的,因此本文重點討論的是二級緩存,下文中提到的緩存若是沒有特別指定的話都是指二級緩存。java

自定義緩存

實現自定義緩存很是簡單,只須要實現org.apache.ibatis.cache.Cache接口,而後爲須要的Mapper配置實現就能夠了。
下面的代碼是一個簡單的緩存實現sql

@Slf4j
public class MyCache implements Cache, InitializingObject {
    private String id;
    private String key;
    private Map<Object, Object> table = new ConcurrentHashMap<>();
    
    public MyCache(String id) {
        this.id = id;
    }

    @Override
    public void initialize() throws Exception {
        log.info("id = {}", id);
        log.info("key = {}", key);
    }
    // ......
}
複製代碼

使用註解方式爲Mapper配置緩存,使用XML配置也是相似的apache

@Mapper
@CacheNamespace(
        // 指定實現類
        implementation = MyCache.class,
        // 指定淘汰策略(也實現了Cache接口),mybatis經過裝飾者模式實現淘汰策略
        // 只有當implementation是PerpetualCache時纔會生效
        eviction = LruCache.class,
        // 配置緩存屬性,mybatis會將對應的屬性注入到緩存對象中
        properties = {
                @Property(name = "key", value = "hello mybatis")
        }
)
public interface AddressMapper {
    // ......
}
複製代碼

緩存對象的建立

緩存是什麼時候建立的呢?咱們不妨想一下,緩存是配置在Mapper上的,那麼應該會在解析Mapper的時候順便把緩存配置也解析了吧。咱們不妨先看看Mapper配置解析的代碼,Configuration類添加Mapper時會調用org.apache.ibatis.binding.MapperRegistryaddMapper方法,以下所示,很直觀的,這裏使用了一個叫作MapperAnnotationBuilder的類來解析Mapper註解。緩存

public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) {
        knownMappers.put(type, new MapperProxyFactory<T>(type));
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
    }
  }
複製代碼

那麼咱們關注一下這個類的parse方法,很是棒,咱們一會兒就找到了解析緩存配置的地方。mybatis

public void parse() {
    String resource = type.toString();
    if (!configuration.isResourceLoaded(resource)) {
      loadXmlResource();
      configuration.addLoadedResource(resource);
      assistant.setCurrentNamespace(type.getName());
      // 解析緩存
      parseCache();
      // 解析引用的緩存
      parseCacheRef();
      Method[] methods = type.getMethods();
      for (Method method : methods) {
          if (!method.isBridge()) {
            // 解析生成MappedStatement
            parseStatement(method);
          }
      }
    }
    parsePendingMethods();
  }
複製代碼

parseCache方法也很是直觀,簡單粗暴,取出@CacheNamespace註解中的配置,而後傳遞給MapperBuilderAssistant#useNewCache方法建立緩存對象,MapperBuilderAssistant是構建Mapper的一個輔助類。架構

private void parseCache() {
    CacheNamespace cacheDomain = type.getAnnotation(CacheNamespace.class);
    if (cacheDomain != null) {
      Integer size = cacheDomain.size() == 0 ? null : cacheDomain.size();
      Long flushInterval = cacheDomain.flushInterval() == 0 ? null : cacheDomain.flushInterval();
      // 把屬性配置轉成Properties對象
      Properties props = convertToProperties(cacheDomain.properties());
      assistant.useNewCache(cacheDomain.implementation(), cacheDomain.eviction(), flushInterval, 
                            size, cacheDomain.readWrite(), cacheDomain.blocking(), props);
    }
  }
複製代碼

先把緩存對象添加到配置對象的註冊表中,這樣的話其餘的Mapper就能夠經過配置@CacheNamespaceRef來引用這個緩存對象了。而後設置緩存對象到輔助類的成員變量,在後面建立MappedStatement時候拿出使用。app

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);
    // 設置爲當前Mapper的緩存,後面構建MappedStatement的時候會用到
    currentCache = cache;
    return cache;
  }
複製代碼

而後再看看CacheBuilder#build方法都幹了些啥吧,具體細節我註釋在下面的代碼裏面。ide

public Cache build() {
    // 首先,確保實現類和淘汰策略爲空的時候,設置默認的實現PerpetualCache和LruCache
    setDefaultImplementations();
    // 這裏要求實現的緩存類必須提供一個帶id參數的構造器,否則就會報錯
    Cache cache = newBaseCacheInstance(implementation, id);
    // 設置經過@Property配置的屬性到緩存對象中,而後若是實現了InitializingObject接口還會調用initialize方法
    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;
  }
複製代碼

這個建立好的緩存是如何配置到MappedStatement中去的呢?回到MapperAnnotationBuilder#parse方法找到parseStatement(method),最終會調用到MapperBuilderAssistant#addMappedStatement()方法,下面代碼就會把剛纔建立的緩存對象設置到每一個MappedStatement中去,因而可知mybatis二級緩存的做用域是整個Mapper的(若是被其餘Mapper引用,還會擴張)svg

public MappedStatement addMappedStatement(/* ... */) {
    id = applyCurrentNamespace(id, false);
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
        /* ... */
        .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
        .useCache(valueOrDefault(useCache, isSelect))
        .cache(currentCache);
    /* ... */
    MappedStatement statement = statementBuilder.build();
    configuration.addMappedStatement(statement);
    return statement;
  }
複製代碼

到這裏終因而把咱們自定義的緩存設置到了配置中了,接下來就是緩存的使用了。ui

緩存的使用

從源碼聊聊mybatis一次查詢都經歷了些什麼這篇文章中簡單提到過緩存的使用是在CachingExecutor中。再把代碼貼過來看一看:

public <E> List<E> query(/* ... */) throws SQLException {
  // 這裏就取到前面設置到ms(MappedStatement)中的緩存對象了
  Cache cache = ms.getCache();
  if (cache != null) {
    // 經過上面的配置就能知道,默認狀況下除了select都須要清空
    flushCacheIfRequired(ms);
    if (ms.isUseCache() && resultHandler == null) {
      // 又懵逼了?這個tcm是啥
      List<E> list = (List<E>) tcm.getObject(cache, key);
      // 緩存未命中,查庫
      if (list == null) {
        list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
        tcm.putObject(cache, key, list);
      }
      return list;
    }
  }
  return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
複製代碼

一切都瓜熟蒂落了,不過半路殺出個程咬金,這個tcm(TransactionalCacheManager)是什麼東西呢?看看下面這張圖,mybatis的每次會話(SqlSession)都會建立一個tcm,這個tcm裏面其實維護着一個HashMap,map的key就是Mapper的cache對象,value是一個使用TransactionalCache裝飾的cache對象。 {% asset_img cache.svg mybatis緩存 %} 從名字就能夠猜一猜,這個TransactionalCache應該是和事務有關係的,從下面的代碼能夠看出,putObject操做並無直接添加到緩存中,而是先put到一個本地Map,而後再批量提交。getObject緩存未命中時會把key添加到一個本地的Set中,在將來批量提交的時候會把這個Set中的key也put到緩存中,value設置爲null,來防止緩存穿透。

public class TransactionalCache implements Cache {
  public void putObject(Object key, Object object) {
    entriesToAddOnCommit.put(key, object);
  }
  
  public Object getObject(Object key) {
    Object object = delegate.getObject(key);
    if (object == null) {
      // 未命中key添加到Set中
      entriesMissedInCache.add(key);
    }
    /* ... */
  }

  public void clear() {
    clearOnCommit = true;
    entriesToAddOnCommit.clear();
  }

  public void commit() {
    // clearOnCommit在TransactionalCache#clear方法被調用後設置爲true
    // 此時纔會在提交的時候清空delegate
    if (clearOnCommit) {
      delegate.clear();
    }
    flushPendingEntries();
    reset();
  }

  private void flushPendingEntries() {
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
      delegate.putObject(entry.getKey(), entry.getValue());
    }
    // 爲未命中的key設置null
    for (Object entry : entriesMissedInCache) {
      if (!entriesToAddOnCommit.containsKey(entry)) {
        delegate.putObject(entry, null);
      }
    }
  }
}
複製代碼

至於何時commit會被調用呢,咱們再回看一下TransactionalCacheManager的commit,會提交當前SqlSession全部Mapper的緩存,而TransactionalCacheManager的commit是在CachingExecutor的commit中調用的,而Executor的commit又依賴與SqlSession的commit操做,也就是說,若是咱們不手動調用SqlSession的commit的話,就只能等到SqlSession關閉的時候纔會提交這個查詢緩存。

public void commit() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.commit();
    }
  }
複製代碼

從源碼咱們不難發現CachingExecutor在每次調用update方法的時候,都會先清空TransactionalCache的本地的HashMap,而後在提交的時候再清空Mapper的緩存。所以,在更新操做比較頻繁的場景下,二級緩存反而不會起到很好的做用。因此是否開啓二級緩存,還要取決於業務場景。可能大部分的場景下,關閉二級緩存都是一個比較不錯的方案。

相關文章
相關標籤/搜索