Mybatis源碼學習:一級緩存和二級緩存分析

零、一級緩存和二級緩存的流程

以這裏的查詢語句爲例。

一級緩存總結

  • 以下兩種情況會直接在一級緩存中查找數據

    • 主配置文件或映射文件沒有配置二級緩存開啓。
    • 二級緩存中不存在數據。
  • 根據statetment,生成一個CacheKey。

  • 判斷是否需要清空本地緩存。

  • 根據cachekey從localCache中獲取數據。

  • 如果緩存未命中,走接下來三步並向下

    • 從數據庫查詢結果。
    • 將cachekey:數據存入localcache中。
    • 將數據返回。
  • 如果緩存命中,直接從緩存中獲取數據。

  • localCache的範圍如果爲statement,清空一級緩存。

二級緩存總結

  • 判斷主配置文件是否設置了enabledCache,默認是開啓的,創建CachingExecutor。

  • 根據statetment,生成一個CacheKey。

  • 判斷映射文件中是否有cache標籤,如果沒有則跳過以下針對二級緩存的操作,從一級緩存中查,查不到就從數據庫中查。

  • 否則即開啓了二級緩存,獲取cache。

  • 判斷是否需要清空二級緩存。

  • 判斷該語句是否需要使用二級緩存isUserCache。

  • 如果二級緩存命中,則直接返回該數據。

  • 如果二級緩存未命中,則將cachekey存入未命中set,然後進行一下的操作:

    • 從一級緩存中查,如果命中就返回,沒有命中就從數據庫中查。
    • 將查到的數據返回,並將cachekey和數據(對象的拷貝)存入待加入二級緩存的map中。
  • 最後commit和close操作都會使二級緩存真正地更新。

一、緩存接口Cache及其實現類

緩存類的頂級接口Cache,裏面定義了加入數據到緩存,從緩存中獲取數據,清楚緩存等操作,通常mybatis會將namespace作爲id,將CacheKey作爲Map中的鍵,而map中的值也就是存儲在緩存中的對象。

在這裏插入圖片描述

而通過裝飾器設計模式,將Cache的功能進行加強,在它的實現類中有着明顯的體現:

在這裏插入圖片描述

PerpetualCache:是最基礎的緩存類,採用HashMap實現,同時一級緩存使用的localCache就是該類型。

LruCache:Lru(least recently used),採用Lru算法可以實現移除最長時間沒有使用的key/value。

SerializedCache:提供了序列化功能,將值序列化後存入緩存,用於緩存返回一份實例的Copy,保證線程安全。

LoggingCache:提供日誌功能,如果開啓debugEnabled爲true,則打印緩存命中日誌。

SynchronizedCache:同步的Cache,用synchronized關鍵字修飾所有方法。

下圖可以得知其執行鏈:SynchronizedCache -> LoggingCache -> SerializedCache -> LruCache -> PerpetualCache

在這裏插入圖片描述

二、cache標籤解析源碼

XMLMapperBuilder中的configurationElement負責解析mappers映射文件中的標籤元素,其中有個cacheElement方法,負責解析cache標籤。

  private void cacheElement(XNode context) throws Exception {
    if (context != null) {
      //獲取type屬性,默認爲perpetual
      String type = context.getStringAttribute("type", "PERPETUAL");
      //獲取type類對象
      Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
      //獲取eviction策略,默認爲lru,即最近最少使用,移除最長時間不被使用的對象
      String eviction = context.getStringAttribute("eviction", "LRU");
      Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
      //獲取flushInterval刷新間隔
      Long flushInterval = context.getLongAttribute("flushInterval");
      //獲取size引用數目
      Integer size = context.getIntAttribute("size");
      //獲取是否只讀
      boolean readWrite = !context.getBooleanAttribute("readOnly", false);
      //獲取是否blocking
      boolean blocking = context.getBooleanAttribute("blocking", false);
      //這一步是另外一種設置cache的方式,即cache子元素中用property,name,value定義
      Properties props = context.getChildrenAsProperties();
      builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
  }

getStringAttribute方法,這個方法的作用就是獲取指定的屬性值,如果沒有設置的話,就採用默認的值:

  public String getStringAttribute(String name, String def) {
    //獲取name參數對應的屬性
    String value = attributes.getProperty(name);
    if (value == null) {
      //如果沒有設置,默認爲def
      return def;
    } else {
      return value;
    }
  }

resolveAlias方法,從源碼中我們就可以猜測,我們之前通過</typeAliases>起別名其實也就是將裏面的內容解析,並存入map之中,而每次處理類型的時候,都比較的是小寫的形式,這也是我們起別名之後不用關心大小寫的原因。

  // throws class cast exception as well if types cannot be assigned
  public <T> Class<T> resolveAlias(String string) {
    try {
      if (string == null) {
        return null;
      }
      //首先將傳入的參數轉換爲小寫形式
      String key = string.toLowerCase(Locale.ENGLISH);
      Class<T> value;
      //到TypeAliasRegistry維護的Map,TYPE_ALIASES中找有無對應的鍵
      if (TYPE_ALIASES.containsKey(key)) {
        //找到就直接返回:class類對象
        value = (Class<T>) TYPE_ALIASES.get(key);
      } else {
        //找不到就通過反射獲取一個
        value = (Class<T>) Resources.classForName(string);
      }
      return value;
    } catch (ClassNotFoundException e) {
      throw new TypeException("Could not resolve type alias '" + string + "'.  Cause: " + e, e);
    }
  }

根據獲取的屬性,通過裝飾器模式,層層裝飾,最後創建了一個SynchronizedCache,並添加到configuration中。因此我們可以知道,一旦我們在映射文件中設置了<cache>,就會創建一個SynchronizedCache緩存對象。

  public Cache useNewCache(Class<? extends Cache> typeClass,
      Class<? extends Cache> evictionClass,
      Long flushInterval,
      Integer size,
      boolean readWrite,
      boolean blocking,
      Properties props) {
    //把當前的namespace當作緩存的id
    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();
    //將cache加入configuration
    configuration.addCache(cache);
    currentCache = cache;
    return cache;
  }

三、CacheKey緩存項的key

默認情況下,enabledCache的全局設置是開啓的,所以Executor會創建一個CachingExecutor,以查詢爲例,當執行Executor實現類的時候,會獲取boundsql,並根據當前信息創建緩存項的key。

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    //從MappedStatement中獲取boundsql
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    //Cachekey類表示緩存項的key
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }  

每一個SqlSession中持有了自己的Executor,每一個Executor中有一個Local Cache。當用戶發起查詢時,Mybatis會根據當前執行的MappedStatement生成一個key,去Local Cache中查詢,如果緩存命中的話,返回。如果緩存沒有命中的話,則寫入Local Cache,最後返回結果給用戶。

boundsql對象的詳細信息:

在這裏插入圖片描述

CacheKey對象的CreateKey操作:

  • 首先創建一個cachekey,默認hashcode=17,multiplier=37,count=0,updateList初始化。
  • update操作:count++,對checksum,hashcode進行賦值,最後將參數添加到updatelist中。
  //根據傳入信息,創建chachekey
  @Override
  public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    //執行器關閉就拋出異常
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    //創建一個cachekey,默認hashcode=17,multiplier=37,count=0,updateList初始化
    CacheKey cacheKey = new CacheKey();
    //添加操作:sql的id,邏輯分頁偏移量,邏輯分頁起始量,sql語句。
    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);
      }
    }
    //添加environment的id名,如果它不爲空的話
    if (configuration.getEnvironment() != null) {
      // issue #176
      cacheKey.update(configuration.getEnvironment().getId());
    }
    //返回cachekey
    return cacheKey;
  }

所以緩存項的key最後表示爲:hashcode:checknum:遍歷updateList,以:間隔

2020122321:657338105:com.smday.dao.IUserDao.findById:0:2147483647:select * from user where id = ?:41:mysql


接着,調用同類中的query方法,針對是否開啓二級緩存做不同的決斷。(需要注意的是,這一部分是建立在cacheEnabled設置爲true的前提下,當然默認是true。如果爲false,Executor將會創建BaseExecutor,並不會判斷mappers映射文件中二級緩存是否存在,而是直接執行delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql)

  //主配置文件已經開啓二級緩存
  @Override
  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) {
      //如果cache不爲空,且需要清緩存的話(insert|update|delete),執行tcm.clear(cache);
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, parameterObject, boundSql);
        @SuppressWarnings("unchecked")
        //從緩存中獲取
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          //緩存中沒有就執行查詢,BaseExecutor的query
          list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          //存入緩存
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        //如果緩存中有,就直接返回
        return list;
      }
    }
    //映射文件沒有開啓二級緩存,需要進行查詢,delegate其實還是Executor對象
    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

除了select操作之外,其他的的操作都會清空二級緩存。XMLStatementBuilder中配置屬性的時候:boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);

  private void flushCacheIfRequired(MappedStatement ms) {
    Cache cache = ms.getCache();
    if (cache != null && ms.isFlushCacheRequired()) {
      //tcm後面會總結,清空二級緩存
      tcm.clear(cache);
    }
  }

四、二級緩存TransactionCache

這裏學習一下二級緩存涉及的緩存類:TransactionCache,同樣也是基於裝飾者設計模式,對傳入的Cache進行裝飾,構建二級緩存事務緩衝區:

在這裏插入圖片描述

CachingExecutor維護了一個TransactionCacheManager,即tcm,而這個tcm其實維護的就是一個key爲Cache,value爲TransactionCache包裝過的Cache。而tcm.getObject(cache, key)的意思我們可以通過以下源碼得知:

  public Object getObject(Cache cache, CacheKey key) {
    //將傳入的cache包裝爲TransactionalCache,並根據key獲取值
    return getTransactionalCache(cache).getObject(key);
  }

需要注意的是,getObject方法中將會把獲取值的職責一路向後傳遞,直到最基礎的perpetualCache,根據cachekey獲取。

在這裏插入圖片描述

最終獲取到的值,如果爲null,就需要把key加入未命中條目的緩存。

  @Override
  public Object getObject(Object key) {
    //根據職責一路向後傳遞
    Object object = delegate.getObject(key);
    if (object == null) {
      //沒找到值就將key存入未命中的set
      entriesMissedInCache.add(key);
    }
    // issue #146
    if (clearOnCommit) {
      return null;
    } else {
      return object;
    }
  }

如果緩存中沒有找到,將會從數據庫中查找,查詢到之後,將會進行添加操作,也就是:tcm.putObject(cache, key, list);。我們可以發現,其實它並沒有直接將數據加入緩存,而是將數據添加進待提交的map中。

  @Override
  public void putObject(Object key, Object object) {
    entriesToAddOnCommit.put(key, object);
  }

也就是說,一定需要某種手段才能讓他真正地存入緩存,沒錯了,commit是可以的:

  //CachingExecutor.java
  @Override
  public void commit(boolean required) throws SQLException {
    //清除本地緩存
    delegate.commit(required);
    //調用tcm.commit
    tcm.commit();
  }

最終調用的是TransactionCache的commit方法:

  public void commit() {
    if (clearOnCommit) {
      delegate.clear();
    }
    flushPendingEntries();
    reset();
  }

最後的最後,我們可以看到將剛纔的未命中和待提交的數據都進行了相應的處理,這纔是最終影響二級緩存中數據的操作,當然這中間也存在着職責鏈,就不贅述了。

在這裏插入圖片描述

當然,除了commit,close也是一樣的,因爲最終調用的其實都是commit方法,同樣也會操作緩存。

五、二級緩存測試

    <!-- 開啓全局配置 -->
    <settings>
        <!--全局開啓緩存配置,是默認開啓的-->
        <setting name="cacheEnabled" value="true"/>
    </settings>

    <!-- 映射配置文件 -->
    <!--開啓user支持二級緩存-->
    <cache></cache>

    <select id="findById" resultType="user" useCache="true" >
        select * from user where id = #{id}
    </select>

    /**
     * 測試二級緩存
     */
    @Test
    public void testFirstLevelCache2(){
        SqlSession sqlSession1 = factory.openSession();
        IUserDao userDao1 = sqlSession1.getMapper(IUserDao.class);
        User user1 = userDao1.findById(41);
        System.out.printf("==> %s\n", user1);
        sqlSession1.commit();
        //sqlSession1.close();
        
        SqlSession sqlSession2 = factory.openSession();
        IUserDao userDao2 = sqlSession2.getMapper(IUserDao.class);
        User user2 = userDao2.findById(41);
        System.out.printf("==> %s\n", user2);
        sqlSession2.close();
        System.out.println("user1 == user2:"+(user1 == user2));
        
        SqlSession sqlSession3 = factory.openSession();
        IUserDao userDao3 = sqlSession3.getMapper(IUserDao.class);
        User user3 = userDao3.findById(41);
        System.out.printf("==> %s\n", user3);
        sqlSession2.close();
        System.out.println("user2 == user3:"+(user2 == user3));
    }

在這裏插入圖片描述

二級緩存實現了SqlSession之間緩存數據的共享,是mapper映射級別的緩存。

有時緩存也會帶來數據讀取正確性的問題,如果數據更新頻繁,會導致從緩存中讀取到的數據並不是最新的,可以關閉二級緩存。

六、一級緩存源碼解析

主配置文件或映射文件沒有配置二級緩存開啓,或者二級緩存中不存在數據,最終都會執行BaseExecutor的query方法,如果queryStack爲空或者不是select語句,就會先清空本地的緩存。

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

查看本地緩存(一級緩存)是否有數據,如果有直接返回,如果沒有,則調用queryFromDatabase從數據庫中查詢。

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

判斷本地緩存的級別是否爲STATEMENT級別,如果是的話,清空緩存,因此STATEMENT級別的一級緩存無法共享localCache。

      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }

七、測試一級緩存

    /**
     * 測試一級緩存
     */
    @Test
    public void testFirstLevelCache1(){
        SqlSession sqlSession1 = factory.openSession();
        IUserDao userDao1 = sqlSession1.getMapper(IUserDao.class);
        User user1 = userDao1.findById(41);
        System.out.printf("==> %s\n", user1);

        IUserDao userDao2 = sqlSession1.getMapper(IUserDao.class);
        User user2 = userDao2.findById(41);
        System.out.printf("==> %s\n", user2);
        sqlSession1.close();
        System.out.println("user1 == user2:"+(user1 == user2));
    }

在這裏插入圖片描述

一級緩存默認是sqlSession級別地緩存,insert|delete|update|commit()和close()的操作的執行都會清空一級緩存。

怎麼說呢,分析源碼的過程讓我對Mybatis有了更加深刻的認識,可能有些理解還是沒有很到位,或許是經驗不足,很多東西還是浮於表面,但一翻debug下來,看到自己之前一個又一個的迷惑被非常確切地解開,真的爽!

https://www.jianshu.com/p/c553169c5921

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