Mybatis-聊一聊雞肋的快取體系

吾乃上將軍邢道榮發表於2019-04-24

眾所周知mybatis快取體系分為一級快取和二級快取,所以今天就分別聊聊這兩級快取。

一級快取

一級快取的使用是不需要任何配置的,直接使用session就可以使用一級快取。程式碼如下:

  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    }...
    }
  }
 
   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);
 }

  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ...
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      ...
    return list;
  }
 
   private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      localCache.removeObject(key);
    }
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }
複製程式碼

localCache就是所謂的一級快取。

this.localCache = new PerpetualCache("LocalCache");

public class PerpetualCache implements Cache {

  private final String id;

  private Map<Object, Object> cache = new HashMap<Object, Object>();

  public PerpetualCache(String id) {
    this.id = id;
  }
 }
複製程式碼

從程式碼中可以看出localCache是一個PerpetualCache的實現類。在該類裡面,存了一個hashmap,這便是一級快取。看到這兒大家會發現一級快取是如此簡單,以至於沒有任何的淘汰,過期策略! 如果一直執行查詢的話,一級快取會不斷增加。那麼什麼時候一級快取會清空呢?兩種情況:

  1. 執行增,刪,改,之類的寫操作的時候,一級快取會清空。
  2. 當session執行commit的時候,一級快取會清空。

看到這兒大家會發現,一級快取並不好用。

  1. 首先每次修改資料庫的時候,快取都會被清空 對,是清空!
  2. 其次,因為沒有過期,淘汰之類的策略,長時間的查詢會導致快取變得異常龐大。只能通過session的commit操作來清空快取,對又是清空!

這邊還有個細節就是當mybatis與spring整合的時候,mybatis在SqlSessionTemplate類中給session封裝了一層SqlSessionInterceptor,而這個類中有這樣一個邏輯。

  private class SqlSessionInterceptor implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      ...
        Object result = method.invoke(sqlSession, args);
        if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
          sqlSession.commit(true);
        }
        return result;
      ...
    }
  }
複製程式碼

如果該執行緒沒有開啟事務則執行sqlSession.commit(true)。也就是清空一級快取。所以大家在spring中使用mybatis的話,是不用擔心一級快取的問題的,因為每次操作後都會刪掉。當然在spring中你也別想使用一級快取。

二級快取

二級快取使用需要開啟一下配置,首先在mapper檔案中新增cache配置。

<mapper namespace="***">
    <cache/>
</mapper>
複製程式碼

其次資料bean還需要實現Serializable介面。這樣就可以開啟二級快取了。二級快取程式碼如下:

  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } ...
    }
  }
 
   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);
  }
 
   public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    Cache cache = ms.getCache();
    ...
        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;
      }
      ...
  }
複製程式碼

這個地方cache就是二級快取。當然二級快取在使用上和一級快取稍有區別。一級快取執行session.commit()之後,快取就清空了。二級快取則必須執行session.commit()資料才會被真正的快取下來。這邊可以看一下 tcm.putObject(cache, key, list); 這個方法

  public void putObject(Cache cache, CacheKey key, Object value) {
    getTransactionalCache(cache).putObject(key, value);
  }
 
   public void putObject(Object key, Object object) {
   資料僅僅是被存到了entriesToAddOnCommit這個裡面
    entriesToAddOnCommit.put(key, object);
  }
 
 session.commit();

  public void commit(boolean force) {
    try {
      executor.commit(isCommitOrRollbackRequired(force));
      dirty = false;
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error committing transaction.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }
 
   public void commit(boolean required) throws SQLException {
    delegate.commit(required);
    //這個方法才是真正存資料的方法
    tcm.commit();
  }
 
   public void commit() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.commit();
    }
  }
 
   public void commit() {
    if (clearOnCommit) {
      delegate.clear();
    }
    flushPendingEntries();
    reset();
  }
 
   private void flushPendingEntries() {
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
      delegate.putObject(entry.getKey(), entry.getValue());
    }
    for (Object entry : entriesMissedInCache) {
      if (!entriesToAddOnCommit.containsKey(entry)) {
        delegate.putObject(entry, null);
      }
    }
  }
複製程式碼

二級快取是帶有過期和淘汰策略的。進入XMLMapperBuilder的cacheElement方法:

  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();
      builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
  }
複製程式碼

這個地方可以看到和快取相關的配置以及預設值。這邊邏輯比較簡單大家自己看看就好。

那麼相比於一級快取而言,擁有了過期,淘汰等策略,還可以自定義的二級快取是不是就好用一點了呢?至少我覺得不是,因為翻看程式碼可以發現,在預設情況下,每次執行增刪改等操作的時候,二級快取也會被清空。這等於手動觸發快取雪崩啊。當然你可以自己定義,但是因為CacheKey是固定生成模式,想要自己定義,得自己解析相應的key或者結果集,還是比較麻煩的。

所以綜上所述,我個人認為mybatis的快取體系比較雞肋,不實用。


返回目錄

相關文章