mybatis快取機制
目錄
mybatis快取機制
mybatis支援一、二級快取來提高查詢效率,能夠正確的使用快取的前提是熟悉mybatis的快取實現原理;
眾所周知,mybatis的sqlSession封裝了對資料庫的增刪改查操作,但是每個SqlSession持有各自的Executor,真正的操作是委託給Executor操作的,而快取功能也同樣是交給了Executor實現;
Executor和快取
下面看一段Configuration類建立執行器的程式碼:
public Executor newExecutor(Transaction transaction, ExecutorType executorType) { 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); } //如果開啟了快取則使用CachingExecutor裝飾 //cacheEnabled實際上是二級快取開關,預設也是開啟的 //只是二級快取需要額外的配置所有並不生效 if (cacheEnabled) { executor = new CachingExecutor(executor); } executor = (Executor) interceptorChain.pluginAll(executor); return executor; }
mybatis可選配置的執行器有三種,分別是SimpleExecutor、ReuseExecutor和BatchExecutor,預設是SimpleExecutor;除此之外還有一個重要的執行器是CachingExecutor,根據名稱即可推斷它與快取是相關的;看類圖:

我們發現BaseExecutor和CachingExecutor實現了Executor介面,BaseExecutor是一個抽象類,它有三個子類(實際上還有一個ClosedExecutor)
一級快取
mybatis一級快取是在BaseExecutor中實現的,也相當於一級快取是預設開啟的;Cache物件是在BaseExecutor構造方法中建立的,因此一個Executor對應一個locaCache,下面看一下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++; //從一級快取中取快取(我們通常的查詢中是不需要resultHandler的) list = resultHandler == null ? (List<E>) localCache.getObject(key) : null; if (list != null) { //handleLocallyCachedOutputParameters這個只對儲存過程有效 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 //如果一級快取的範圍是statement級別,則每次查詢都清空一級快取 if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { clearLocalCache(); // issue #482 } } return list; }
因此,在不考慮二級快取的情況下,每次查詢都從一級快取中取,如果沒有命中快取,則從資料庫查詢,並將查詢結果加入快取;這只是一級快取的存取,接下來還要知道快取何時失效,其實我們可以推測一下,如果資料庫更新了,但是快取並沒有失效,那麼快取的資料就成了髒資料,所以快取失效肯定和更新操作有關,但是這個更新就有範圍了,是更新操作清除所有快取(全域性)?還是同一個SQLSession的更新操作清除當前SQLSession的快取呢?
通過文件和原始碼我們知道LocalCacheScope有兩個級別,分別是statement和session;從query方法已經知道statement級別每次查詢都清除快取,這也是一級快取預設的級別;
那麼session級別呢?
下面看BaseExecutor的update方法(SqlSesssion的insert、update、delete操作最後都會執行此方法):
public int update(MappedStatement ms, Object parameter) throws SQLException { ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId()); if (closed) throw new ExecutorException("Executor was closed."); //清除快取 clearLocalCache(); return doUpdate(ms, parameter); }
可以看到如果是session級別,在update操作的時候清除快取;但是有兩點要注意:
一、為什麼叫做session級別?
同一個SqlSession持有同一個Executor,同一個Executor持有同一個LocalCache,clearLocalCache操作只是清除當前executor的本地快取,因此session級別的快取就是對同一個SqlSession生效。
二、快取失效的時機
可以看到清除快取是在doUpdate(真正的更新操作)操作之前執行的,也就是說doUpdate執行成功或失敗、提交或者回滾 快取都會失效;
小結
- MyBatis一級快取使用沒有容量限制的HashMap,比較簡陋;
- statement級別的快取每一次查詢後清除;
- session級別快取在同一個SqlSession的insert、update、delete操作之前清除;
- MyBatis的一級快取最大是同一個SqlSession,在多個SqlSession環境下就會出現資料修改後快取無法及時失效的情況產生髒資料;
二級快取
前面我們知道二級快取開啟後Executor會使用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) { //是否立即清除快取,這個是statement標籤中flushCache屬性控制的,select標籤預設false,其它標籤預設true; flushCacheIfRequired(ms); if (ms.isUseCache() && resultHandler == null) { //關於儲存過程暫不考慮 //isUseCache()的值是statement標籤中useCache配置的,預設為true 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); }
這裡從查詢快取和加入快取用的是tcm(TransactionalCacheManager)的getObject和putObject方法,稍稍看一下這個類:
public class TransactionalCacheManager { //維護TransactionalCache 和 Cache 一對一的這樣一個對映關係 private Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>(); //清除快取 public void clear(Cache cache) { getTransactionalCache(cache).clear(); } //從快取獲取結果 public Object getObject(Cache cache, CacheKey key) { return getTransactionalCache(cache).getObject(key); } //加入快取(真正加入還要等commit) public void putObject(Cache cache, CacheKey key, Object value) { getTransactionalCache(cache).putObject(key, value); } //省略一部分 。。。。。。。 private TransactionalCache getTransactionalCache(Cache cache) { TransactionalCache txCache = transactionalCaches.get(cache); if (txCache == null) { //使用TransactionalCache裝飾Cache txCache = new TransactionalCache(cache); transactionalCaches.put(cache, txCache); } return txCache; } }
這裡我們只需要知道關於快取的操作最終還是委託給Cache類的,其它的暫不深入,回到CacheExecutor類,Cache物件是從MappedStatement(對應就是select、update等sql標籤)中獲取的,而Cache也不是在MappedStatement中建立的,但是我們知道mybatis的namespace中關於快取有如下兩個標籤:
//表示此namespace要使用二級快取 <cache/> 屬性 type:cache使用的型別,預設是PerpetualCache; eviction: 快取策略,常見的有FIFO,LRU; flushInterval: 自動重新整理快取時間間隔,單位是毫秒。 size: 快取的物件數量最大值。 readOnly: 是否只讀,false時需要實現Serializable介面,預設false。 blocking: 若快取中找不到對應的key,是否會一直blocking,直到有對應的資料進入快取。 //引用其它namespace的快取 <cache-ref namespace="mapper.StudentMapper"/>
可以猜測,Cache的建立在解析namespace標籤之後,所以從XmlConfigBuilder(解析配置檔案的關鍵類)一路找到XMLMapperBuilder(根據名稱就知道是解析mapper相關的配置也就是namespace標籤下的內容):
//建立快取物件 private void cacheElement(XNode context) throws Exception { if (context != null) { //獲取<cache/>標籤配置 .... //建立Cache物件 builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, props); } }
接著看builderAssistant的useNewCache方法:
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); //將namespace作為Cache的id Cache cache = new CacheBuilder(currentNamespace) .implementation(typeClass) .addDecorator(evictionClass) .clearInterval(flushInterval) .size(size) .readWrite(readWrite) .properties(props) .build(); //將Cache放入Configuration中 //Configuration中維護一個Map,鍵是Cache的id也就是namespace configuration.addCache(cache); currentCache = cache; return cache; }
這裡我們知道解析namespace的cache標籤馬上會為此namespace建立一個Cache物件;那麼cache-ref標籤呢?同樣是XMLMapperBuilder類:
private void cacheRefElement(XNode context) { if (context != null) { configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace")); CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace")); try { cacheRefResolver.resolveCacheRef(); } catch (IncompleteElementException e) { configuration.addIncompleteCacheRef(cacheRefResolver); } } }
Configuration類有一個map儲存的是cache-ref標籤宣告的引用關係,CacheRefResolver就是去獲取引用的namespace的Cache物件,這時如果引用的Cache還沒有建立怎麼辦?mybatis是將它放在了IncompleteCacheRef的集合中,最後再去重新去處理引用;到這裡我們知道了Cache的建立,但是我還記得CacheExecutor中的Cache是從MappedStatement中取的啊!那是因為
XMLStatementBuilder在建立namespace下的MappedStatement時候就將XMLMapperBuilder中建立的Cache注入其中了,因此同一個namespace下的MappedStatement持有的是同一個Cache物件,如果namespace之間是引用關係,那麼也是同一個Cache物件;到這裡已經弄清楚了MappedStatement中Cache的來歷;
再回到CachingExecutor中的清除快取的方法:
private void flushCacheIfRequired(MappedStatement ms) { Cache cache = ms.getCache(); if (cache != null && ms.isFlushCacheRequired()) { tcm.clear(cache); } }
ms.isFlushCacheRequired()的值是statement標籤中flushCache屬性控制的,select標籤預設false,其它標籤預設true;
這裡clear方法並沒有清除快取,而是設定了一個標誌位 clearOnCommit = true;顧名思義在提交的時候清除;除此之外,
tcm(TransactionalCacheManager)的put和remove操作也只是將動作臨時存放在map中,commit 的時候才真正執行:
public void commit() { if (clearOnCommit) { //清除快取 delegate.clear(); } else { //執行暫存的操作 for (RemoveEntry entry : entriesToRemoveOnCommit.values()) { entry.commit(); } } for (AddEntry entry : entriesToAddOnCommit.values()) { entry.commit(); } reset(); } //rollback重置,不對快取操作 public void rollback() { reset(); }
再簡單說一下關於Cache介面:
Cache的設計使用了裝飾器模式,基本的裝飾鏈是:
SynchronizedCache -> LoggingCache -> SerializedCache -> LruCache -> PerpetualCache。
具體的過程可以去看CacheBuilder類的build方法;mybatis預設的cache標籤type屬性是PerpetualCache、eviction是lru,如果要自定義快取只需要實現Cache介面,並做相應配置即可;
小結
- 二級快取的有效範圍是namespace,使用cache-ref標籤可以實現多個namespace共享快取;
- 二級快取可以根據statement標籤的useCache和flushCache 細粒度的控制是否需要使用快取和強制重新整理快取
- 二級快取的實現相對於一級快取有明顯增強,但是依然是本地實現,解決了多個SqlSession共享快取的問題,但是仍然無法應用於分散式環境;
- 由於是基於namespace的快取,如果存在多表查詢,可能存在資料更新之後此namespace下的快取還沒有失效,也會產生髒資料;
- 總的來說,如果不熟悉mybatis的快取機制,最好是使用第三方快取;