1. 程式人生 > >Mybatis 原始碼分析(8)—— 一二級快取

Mybatis 原始碼分析(8)—— 一二級快取

一級快取

其實關於 Mybatis 的一級快取是比較抽象的,並沒有什麼特別的配置,都是在程式碼中體現出來的。

當呼叫 Configuration 的 newExecutor 方法來建立 executor:

public Executor newExecutor(Transaction transaction, ExecutorType executorType, boolean autoCommit) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null
? ExecutorType.SIMPLE : executorType; Executor executor; if (ExecutorType.BATCH == executorType) { executor = new BatchExecutor(this, transaction); } else if (ExecutorType.REUSE == executorType) { executor = new ReuseExecutor(this, transaction); } else { executor = new
SimpleExecutor(this, transaction); } if (cacheEnabled) { executor = new CachingExecutor(executor, autoCommit); } // 執行對外掛的呼叫 executor = (Executor) interceptorChain.pluginAll(executor); return executor; }

預設的 executorType 是 ExecutorType.SIMPLE(SimpleExecutor)。cacheEnabled 預設為 true ,所以一般情況下都會建立 CachingExecutor。

當我們要使全域性的對映器禁用快取,可以配置 cacheEnabled 為false:

在 CacheingExecutor 的 query 方法中:

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

即使沒有建立 CachingExecutor,在 BaseExecutor 的 query 方法中同樣操作:

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);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

不同的是,CachingExecutor 會在 MappedStatement 中獲取 Cache,如果為 null,直接呼叫 BaseExecutor 的 query 方法:

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();
      }
      deferredLoads.clear(); // issue #601
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        clearLocalCache(); // issue #482
      }
    }
    return list;
}

可以看到預設使用了 localCache,這個 localCache 是 PerpetualCache 型別的,基於 HashMap 實現。不管是使用哪種 Cache,CacheKey 都是通過 BaseExecutor 來建立:

public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) throw new ExecutorException("Executor was closed.");
    CacheKey cacheKey = new CacheKey();
    cacheKey.update(ms.getId());
    cacheKey.update(rowBounds.getOffset());
    cacheKey.update(rowBounds.getLimit());
    cacheKey.update(boundSql.getSql());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings.size() > 0 && parameterObject != null) {
      TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
      if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
        cacheKey.update(parameterObject);
      } else {
        MetaObject metaObject = configuration.newMetaObject(parameterObject);
        for (ParameterMapping parameterMapping : parameterMappings) {
          String propertyName = parameterMapping.getProperty();
          if (metaObject.hasGetter(propertyName)) {
            cacheKey.update(metaObject.getValue(propertyName));
          } else if (boundSql.hasAdditionalParameter(propertyName)) {
            cacheKey.update(boundSql.getAdditionalParameter(propertyName));
          }
        }
      }
    }
    return cacheKey;
}

這個 CacheKey 主要使用 hashCode 來構建唯一標識,預設的 hashCode 為 17,每一次 update 都會更新這個 hashCode :

  public void update(Object object) {
    int baseHashCode = object == null ? 1 : object.hashCode();

    count++;
    checksum += baseHashCode;
    baseHashCode *= count;

    hashcode = multiplier * hashcode + baseHashCode;

    updateList.add(object);
  }

如果一個查詢的 id、分頁元件中的 offset 和 limit、sql 語句、引數 都保持不變,那麼這個查詢產生的 CacheKey一定是不變的。

在一個 SqlSession 的生命週期內,二次同樣的查詢 CacheKey 是一樣的:

-1182036712:853128989:com.fcs.demo.dao.UserMapper.selectUserMaps:0:2147483647:select * from tb_user

-1182036712:853128989:com.fcs.demo.dao.UserMapper.selectUserMaps:0:2147483647:select * from tb_user

為什麼強調是在一個 SqlSession 的生命週期內? PerpetualCache 型別的 localCache 被 Executor 持有,而特定型別的 Executor 又是被 DefaultSqlSession 持有,當 SqlSession 被關閉後,這些都不復存在。

所以這個 localCache 就是 Mybatis 的一級快取,不受任何配置影響,SqlSession 級別的。

二級快取

一開始聽說 MyBatis 的一二級快取,我以為是兩種完全沾不上邊的東西,後來發現這二者竟然在同一個方法裡碰過面,那就是 CachingExecutor 的 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, key, parameterObject, boundSql);
        if (!dirty) {
          cache.getReadWriteLock().readLock().lock();
          try {
            @SuppressWarnings("unchecked")
            List<E> cachedList = (List<E>) cache.getObject(key);
            if (cachedList != null) return cachedList;
          } finally {
            cache.getReadWriteLock().readLock().unlock();
          }
        }
        List<E> list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
        tcm.putObject(cache, key, list); // issue #578. Query must be not synchronized to prevent deadlocks
        return list;
      }
    }
    return delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

首先 MappedStatement 中獲取 cache,這個 cache 就是所謂的二級快取,如果這個 cache 存在,將優先去這個 cache 中查詢,如果找不到結果,那就走一級快取的路子。

突然覺得這個設計很棒啊,有點層層篩選的意思,這個篩網就是特定的 CacheKey,二級快取篩出來了,就不需要再到一級快取去篩了,如果一級也篩不出來,那就掉到最下面的容器裡去了(資料庫)。

那麼二級快取是什麼級別的?這個就要看 Cache 的來源了,上面顯示是從 MappedStatement 中取出來的。而 MappedStatement 是通過 MapperBuilderAssistant 的 addMappedStatement 方法構建的:

setStatementCache(isSelect, flushCache, useCache, currentCache, statementBuilder);

這個方法有三個引數值得關注:flushCache、useCache、currentCache。

而 currentCache 在下面這個方法中可以賦值(還有參照快取相關的 useCacheRef 方法):

public Cache useNewCache(Class<? extends Cache> typeClass,
      Class<? extends Cache> evictionClass,
      Long flushInterval,
      Integer size,
      boolean readWrite,
      Properties props) {
    typeClass = valueOrDefault(typeClass, PerpetualCache.class);
    evictionClass = valueOrDefault(evictionClass, LruCache.class);
    Cache cache = new CacheBuilder(currentNamespace)
        .implementation(typeClass)
        .addDecorator(evictionClass)
        .clearInterval(flushInterval)
        .size(size)
        .readWrite(readWrite)
        .properties(props)
        .build();
    configuration.addCache(cache);
    currentCache = cache;
    return cache;
}

useNewCache 方法是在解析 XML 檔案的時候呼叫的:

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);
      Properties props = context.getChildrenAsProperties();
      builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, props);
    }
}

可以看到如果僅僅配置一個:

<cache/>

將會採用預設的 type – PERPETUAL(PerpetualCache),預設的 eviction – LRU (最近最少使用的)演算法。

官方文件這樣描述:

  • 對映語句檔案中的所有 select 語句將會被快取。
  • 對映語句檔案中的所有 insert,update 和 delete 語句會重新整理快取。
  • 快取會使用 Least Recently Used(LRU,最近最少使用的)演算法來收回。
  • 根據時間表(比如 no Flush Interval,沒有重新整理間隔), 快取不會以任何時間順序來重新整理。
  • 快取會儲存列表集合或物件(無論查詢方法返回什麼)的 1024 個引用。
  • 快取會被視為是 read/write(可讀/可寫)的快取,意味著物件檢索不是共享的,而
    且可以安全地被呼叫者修改,而不干擾其他呼叫者或執行緒所做的潛在修改。

再回到開始的那個全域性的對映器快取是否啟用的配置,如果 cacheEnabled 為 false,那個 CachingExecutor 就不會建立,即使你這裡配置了 cache 也沒有用。

參照快取

某個時候,你會想在名稱空間中共享相同的快取配置和例項。在這樣的情況下你可以使用 cache-ref 元素來引用另外一個快取:

<cache-ref namespace="com.someone.application.data.SomeMapper"/>

在 useCacheRef 方法中是直接按名稱空間去拿的:

public Cache useCacheRef(String namespace) {
    if (namespace == null) {
      throw new BuilderException("cache-ref element requires a namespace attribute.");
    }
    try {
      unresolvedCacheRef = true;
      Cache cache = configuration.getCache(namespace);
      if (cache == null) {
        throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
      }
      currentCache = cache;
      unresolvedCacheRef = false;
      return cache;
    } catch (IllegalArgumentException e) {
      throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
    }
}

快取失效與捨棄

一級快取失效

再回顧下 Mybatis 和 Spring 結合使用時,mybatis-spring 所做的事:

  • MapperFactoryBean 通過繼承 SqlSessionDaoSupport 獲取了 sqlSession:
public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
    if (!this.externalSqlSession) {
      this.sqlSession = new SqlSessionTemplate(sqlSessionFactory);
    }
}
  • SqlSessionTemplate 通過代理來間接操縱 DefaultSqlSession:
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
      PersistenceExceptionTranslator exceptionTranslator) {

    notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
    notNull(executorType, "Property 'executorType' is required");

    this.sqlSessionFactory = sqlSessionFactory;
    this.executorType = executorType;
    this.exceptionTranslator = exceptionTranslator;
    this.sqlSessionProxy = (SqlSession) newProxyInstance(
        SqlSessionFactory.class.getClassLoader(),
        new Class[] { SqlSession.class },
        new SqlSessionInterceptor());
}

動態代理構建了方法的執行模板:

private class SqlSessionInterceptor implements InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      final SqlSession sqlSession = getSqlSession(
          SqlSessionTemplate.this.sqlSessionFactory,
          SqlSessionTemplate.this.executorType,
          SqlSessionTemplate.this.exceptionTranslator);
      try {
        Object result = method.invoke(sqlSession, args);
        if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
          // force commit even on non-dirty sessions because some databases require
          // a commit/rollback before calling close()
          sqlSession.commit(true);
        }
        return result;
      } catch (Throwable t) {
        Throwable unwrapped = unwrapThrowable(t);
        if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
          Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
          if (translated != null) {
            unwrapped = translated;
          }
        }
        throw unwrapped;
      } finally {
        closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
      }
    }
}
  • 通過 SqlSessionUtils 獲取和關閉 SqlSession
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {

    notNull(sessionFactory, "No SqlSessionFactory specified");
    notNull(executorType, "No ExecutorType specified");

    SqlSessionHolder holder = (SqlSessionHolder) getResource(sessionFactory);

    if (holder != null && holder.isSynchronizedWithTransaction()) {
      if (holder.getExecutorType() != executorType) {
        throw new TransientDataAccessResourceException("Cannot change the ExecutorType when there is an existing transaction");
      }

      holder.requested();

      if (logger.isDebugEnabled()) {
        logger.debug("Fetched SqlSession [" + holder.getSqlSession() + "] from current transaction");
      }

      return holder.getSqlSession();
    }

    if (logger.isDebugEnabled()) {
      logger.debug("Creating a new SqlSession");
    }

    SqlSession session = sessionFactory.openSession(executorType);

    //......

    return session;

}

獲取和關閉並不是直接操作 SqlSession,這裡有 SqlSessionHolder,通過 TransactionSynchronizationManager 的 getResource 方法來獲取 SqlSessionHolder,如果 holder 不為 null 並且被當前事物鎖定,則在 holder 中獲取 SqlSession。

public static void closeSqlSession(SqlSession session, SqlSessionFactory sessionFactory) {

    notNull(session, "No SqlSession specified");
    notNull(sessionFactory, "No SqlSessionFactory specified");

    SqlSessionHolder holder = (SqlSessionHolder) getResource(sessionFactory);
    if ((holder != null) && (holder.getSqlSession() == session)) {
      if (logger.isDebugEnabled()) {
        logger.debug("Releasing transactional SqlSession [" + session + "]");
      }
      holder.released();
    } else {
      if (logger.isDebugEnabled()) {
        logger.debug("Closing non transactional SqlSession [" + session + "]");
      }
      session.close();
    }
}

SqlSession 如果重新獲取,必然導致一級快取失效。如果我們自己開啟並關閉 SqlSession,這一切是可控的,但是和 Spring 一起使用時,就要注意這個問題。

二級快取捨棄

看到了二級快取,我不由自主找了一下,在我們的專案中並沒有這個二級快取的配置,這是為什麼?既然可以避免重複查詢,為啥不用呢?

原來不同名稱空間下的表存在關聯查詢的話,其中一個針對某個表做了修改,另外一個名稱空間下的查詢沒有任何變化,還是關聯的這個表,那麼使用了快取明視訊記憶體在髒資料。

所以如果表關聯比較複雜的話,一般是不會使用二級快取的。