mybatis 高速緩存和二級緩存

最近研究標誌映射,提到了最理想的保存標識映射的地方時工作單元,其次是會話。聯想到mybatis的sqlsession。於是做了一個測試,在一個事務方法中不斷調用mapper的查詢方法,觀察日誌打印,看究竟查詢了幾次數據庫。

日誌輸出如下:

DEBUG - Cache Hit Ratio [com.demo.dao.PersonMapper]: 0.0
DEBUG - JDBC Connection [jdbc:mysql://localhost:3306/test, UserName=root@localhost, MySQL-AB JDBC Driver] will be managed by Spring
DEBUG - ==>  Preparing: select id, name, age from person where id = ? 
DEBUG - ==> Parameters: 15(Integer)
DEBUG - <==      Total: 1
DEBUG - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@e63606]
DEBUG - Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@e63606] from current transaction
DEBUG - Cache Hit Ratio [com.demo.dao.PersonMapper]: 0.0
DEBUG - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@e63606]
DEBUG - Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@e63606] from current transaction
DEBUG - Cache Hit Ratio [com.demo.dao.PersonMapper]: 0.0
DEBUG - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@e63606]
sql語句打印一次,只查詢了一次數據庫,後面兩次調用沒有查詢。session期間的確存在一個緩存將結果保存起來。


先看一下MappedStatement初始化。

MapperBuilderAssistant類負責MappedStatement初始化,其中有個setStatementCache代碼中有這麼一句:

statementBuilder.cache(cache);

這裏給MappedStatement的構建者set了cache對象,即SynchronizedCache對象。


ps:SynchronizedCache對象裏面包裝了loggingCache,loggingCache包裝LRUCache,LRUCache包裝PerpetualCache。所以本質還是PerpetualCache。


每個ms就有一個cache,當我們調用查詢方法時,mybatis會選擇Executor,默認cache都是true,因此會使用CachingExecutor。
看看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, parameterObject, boundSql);
        @SuppressWarnings("unchecked")
        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); // issue #578. Query must be not synchronized to prevent deadlocks
        }
        return list;
      }
    }
    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }



這裏取出ms的cache,就是初始化的時候放入的。注意,ms的cache就是二級緩存。


tcm是TransactionalCacheManager,內部有個transactionalCaches保存了一個事物中的cache對象。
private TransactionalCache getTransactionalCache(Cache cache) {
    TransactionalCache txCache = transactionalCaches.get(cache);
    if (txCache == null) {
      txCache = new TransactionalCache(cache);
      transactionalCaches.put(cache, txCache);
    }
    return txCache;
  }


  
這裏把cache封裝到了TransactionalCache裏面去,以cache爲TransactionalCache的key。
tcm.getObject(cache, key);
ps:先調用getTransactionalCache,取得TransactionalCache,在調用TransactionalCache的getObject方法。這裏只是做了一點包裝,本質還是cache。
tcm.getObject(cache, key);就是查詢二級緩存,如果有直接返回結果,如果沒有,則查詢數據庫,保存cache到TransactionalCache。


返回結果。


一級緩存的使用是在二級緩存找不到的情況下,調用BaseExecutor的query()方法中,代碼如下:
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
localCache就是一級緩存,對應每個sqlsession一個。session關閉則消失。


如果一級緩存查找不到,就只能queryFromDatabase了。


好了,現在來解釋一下之前的問題,爲什麼在一個事物中查詢三次同一個對象,使用cache(mapper中配置<cache>),只調用一次數據庫,後面兩次查詢不查數據庫,卻也不命中二級緩存。。


爲了搞清這個問題,我又試過另一個例子。
調用service的queryBykey()三次,service的query事物屬性爲默認,readonly爲true。也就是不能提交。
打印日誌如下:
DEBUG - Cache Hit Ratio [com.demo.dao.PersonMapper]: 0.0
DEBUG - JDBC Connection [jdbc:mysql://localhost:3306/test, UserName=root@localhost, MySQL-AB JDBC Driver] will be managed by Spring
DEBUG - ==>  Preparing: select id, name, age from person where id = ? 
DEBUG - ==> Parameters: 15(Integer)
DEBUG - <==      Total: 1
DEBUG - Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@ecb3f1]
DEBUG - Cache Hit Ratio [com.demo.dao.PersonMapper]: 0.5
DEBUG - Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@d306dd]
DEBUG - Cache Hit Ratio [com.demo.dao.PersonMapper]: 0.6666666666666666


此時發現後面兩次能命中。百思不得其解。不在一個事物卻能命中。


跟蹤代碼半天反覆測試 ,總算被我找到原因。


原來所有一切都是TransactionalCache惹出來的。


仔細看CachingExecutor的query代碼,在查詢二級緩存的時候用的是tcm先去找TransactionalCache然後採取getObject。
問題就在這裏,但我們在一個事物裏查詢三次,第一次查數據庫,這不必說,第二次以後會判斷二級緩存時候有。
第一次查詢完了有這麼一句。
tcm.putObject(cache, key, list); 
跟進去看:

getTransactionalCache(cache).putObject(key, value);


getTransactionalCache(cache)返回TransactionalCache對象,然後調用它的put,是什麼呢


@Override
  public void putObject(Object key, Object object) {
    entriesToRemoveOnCommit.remove(key);
    entriesToAddOnCommit.put(key, new AddEntry(delegate, key, object));
  }

封裝cache在一個addEntry對象中去了。

put方法不是保存數據到TransactionalCache,而是保存cache到entriesToAddOnCommit;那這個entriesToAddOnCommit幹嗎用的呢?


觀察名字就知道是提交事務的時候需要用的。一個方法執行結束,事務提交,session提交,提交是層層調用的。最終調用到CachingExecutor的commit:

public void commit(boolean required) throws SQLException {
    delegate.commit(required);
    tcm.commit();
  }
tcm的commit:

public void commit() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.commit();
    }
  }
把所有TransactionalCache提交,

public void commit() {
    if (clearOnCommit) {
      delegate.clear();
    } else {
      for (RemoveEntry entry : entriesToRemoveOnCommit.values()) {
        entry.commit();
      }
    }
    for (AddEntry entry : entriesToAddOnCommit.values()) {
      entry.commit();
    }
    reset();
  }
AddEntry的commit方法:

public void commit() {
      cache.putObject(key, value);
    }
就是把緩存數據放到二級緩存。

總結就是:

一個事務方法運行時,結果查詢出來,緩存在一級緩存了,但是沒有到二級緩存,事務cache只是保存了二級緩存的引用以及需要緩存的數據key和數據。當事務提交後,事務cache重置,之前保存的本該在二級緩存的數據在此刻真正放到二級緩存。

於是我們在這個方法中反覆查詢,二級緩存啓用了卻不能命中,只能返回一級緩存的數據。要想命中必須提交事務才行,第二個測試每次打開事務,查詢,釋放事務,在獲得事務查詢。所以二級緩存能命中。


最後說一下mybatis自帶緩存的目的不是提高多少性能,而是爲了在一個事務中返回結果同一個對象。



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