MyBatis緩存
我們知道,頻繁的數據庫操作是非常耗費性能的(主要是因為對於DB而言,數據是持久化在磁盤中的,因此查詢操作需要通過IO,IO操作速度相比內存操作速度慢了好幾個量級),尤其是對於一些相同的查詢語句,完全可以把查詢結果存儲起來,下次查詢同樣的內容的時候直接從內存中獲取數據即可,這樣在某些場景下可以大大提升查詢效率。
MyBatis的緩存分為兩種:
一級緩存,一級緩存是SqlSession級別的緩存,對於相同的查詢,會從緩存中返回結果而不是查詢數據庫
二級緩存,二級緩存是Mapper級別的緩存,定義在Mapper文件的<cache>標簽中並需要開啟此緩存,多個Mapper文件可以共用一個緩存,依賴<cache-ref>標簽配置
下面來詳細看一下MyBatis的一二級緩存。
MyBatis一級緩存工作流程
接著看一下MyBatis一級緩存工作流程。前面說了,MyBatis的一級緩存是SqlSession級別的緩存,當openSession()的方法運行完畢或者主動調用了SqlSession的close方法,SqlSession就被回收了,一級緩存與此同時也一起被回收掉了。前面的文章有說過,在MyBatis中,無論selectOne還是selectList方法,最終都被轉換為了selectList方法來執行,那麼看一下SqlSession的selectList方法的實現:
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); } catch (Exception e) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
繼續跟蹤第4行的代碼,到BaseExeccutor的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); }
第3行構建緩存條件CacheKey,這裡涉及到怎麼樣條件算是和上一次查詢是同一個條件的一個問題,因為同一個條件就可以返回上一次的結果回去,這部分代碼留在下一部分分析。
接著看第4行的query方法的實現,代碼位於CachingExecutor中:
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 and #116 } return list; } } return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }
第3行~第16行的代碼先不管,繼續跟第17行的query方法,代碼位於BaseExecutor中:
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--; } ... }
看12行,query的時候會嘗試從localCache中去獲取查詢結果,如果獲取到的查詢結果為null,那麼執行16行的代碼從DB中撈數據,撈完之後會把CacheKey作為key,把查詢結果作為value放到localCache中。
MyBatis一級緩存存儲流程看完了,接著我們從這段代碼中可以得到三個結論:
MyBatis的一級緩存是SqlSession級別的,但是它並不定義在SqlSessio接口的實現類DefaultSqlSession中,而是定義在DefaultSqlSession的成員變量Executor中,Executor是在openSession的時候被實例化出來的,它的默認實現為SimpleExecutor
MyBatis中的一級緩存,與有沒有配置無關,只要SqlSession存在,MyBastis一級緩存就存在,localCache的類型是PerpetualCache,它其實很簡單,一個id屬性+一個HashMap屬性而已,id是一個名為"localCache"的字符串,HashMap用於存儲數據,Key為CacheKey,Value為查詢結果
MyBatis的一級緩存查詢的時候默認都是會先嘗試從一級緩存中獲取數據的,但是我們看第6行的代碼做了一個判斷,ms.isFlushCacheRequired(),即想每次查詢都走DB也行,將<select>標簽中的flushCache屬性設置為true即可,這意味著每次查詢的時候都會清理一遍PerpetualCache,PerpetualCache中沒數據,自然只能走DB
從MyBatis一級緩存來看,它以單純的HashMap做緩存,沒有容量控制,而一次SqlSession中通常來說並不會有大量的查詢操作,因此只適用於一次SqlSession,如果用到二級緩存的Mapper級別的場景,有可能緩存數據不斷碰到而導致內存溢出。
還有一點,差點忘了寫了,<insert>、<delete>、<update>最終都會轉換為update方法,看一下BaseExecutor的update方法:
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); }
第6行clearLocalCache()方法,這意味著所有的增、刪、改都會清空本地緩存,這和是否配置了flushCache=true是無關的。
這很好理解,因為增、刪、改這三種操作都可能會導致查詢出來的結果並不是原來的結果,如果增、刪、改不清理緩存,那麼可能導致讀取出來的數據是髒數據。
一級緩存的CacheKey
接著我們看下一個問題:怎麼樣的查詢條件算和上一次查詢是一樣的查詢,從而返回同樣的結果回去?這個問題,得從CacheKey說起。
我們先看一下CacheKey的數據結構:
public class CacheKey implements Cloneable, Serializable { private static final long serialVersionUID = 1146682552656046210L; public static final CacheKey NULL_CACHE_KEY = new NullCacheKey(); private static final int DEFAULT_MULTIPLYER = 37; private static final int DEFAULT_HASHCODE = 17; private int multiplier; private int hashcode; private long checksum; private int count; private List<Object> updateList; ... }
其中最重要的是第14行的updateList這個兩個屬性,為什麼這麼說,因為HashMap的Key是CacheKey,而HashMap的get方法是先判斷hashCode,在hashCode沖突的情況下再進行equals判斷,因此最終無論如何都會進行一次equals的判斷,看下equals方法的實現:
public boolean equals(Object object) { if (this == object) { return true; } if (!(object instanceof CacheKey)) { return false; } final CacheKey cacheKey = (CacheKey) object; if (hashcode != cacheKey.hashcode) { return false; } if (checksum != cacheKey.checksum) { return false; } if (count != cacheKey.count) { return false; } for (int i = 0; i < updateList.size(); i++) { Object thisObject = updateList.get(i); Object thatObject = cacheKey.updateList.get(i); if (thisObject == null) { if (thatObject != null) { return false; } } else { if (!thisObject.equals(thatObject)) { return false; } } } return true; }
看到整個方法的流程都是圍繞著updateList中的每個屬性進行逐一比較,因此再進一步的,我們要看一下updateList中到底存儲了什麼。
關於updateList裡面存儲的數據我們可以看下哪裡使用了updateList的add方法,然後一步一步反推回去即可。updateList中數據的添加是在doUpdate方法中:
private void doUpdate(Object object) { int baseHashCode = object == null ? 1 : object.hashCode(); count++; checksum += baseHashCode; baseHashCode *= count; hashcode = multiplier * hashcode + baseHashCode; updateList.add(object); }
它的調用方為update方法:
public void update(Object object) { if (object != null && object.getClass().isArray()) { int length = Array.getLength(object); for (int i = 0; i < length; i++) { Object element = Array.get(object, i); doUpdate(element); } } else { doUpdate(object); } }
這裡主要是對輸入參數是數組類型進行了一次判斷,是數組就遍歷逐一做doUpdate,否則就直接做doUpdate。再看update方法的調用方,其實update方法的調用方有挺多處,但是這裡我們要看的是Executor中的,看一下BaseExecutor中的createCacheKey方法實現:
... CacheKey cacheKey = new CacheKey(); cacheKey.update(ms.getId()); cacheKey.update(rowBounds.getOffset()); cacheKey.update(rowBounds.getLimit()); cacheKey.update(boundSql.getSql()); ...
到了這裡應當一目了然了,MyBastis從三個維度判斷兩次查詢是相同的:
<select>標簽所在的Mapper的Namespace+<select>標簽的id屬性
RowBounds的offset和limit屬性,RowBounds是MyBatis用於處理分頁的一個類,offset默認為0,limit默認為Integer.MAX_VALUE
<select>標簽中定義的sql語句
即只要兩次查詢滿足以上三個條件且沒有定義flushCache="true",那麼第二次查詢會直接從MyBatis一級緩存PerpetualCache中返回數據,而不會走DB。
MyBatis二級緩存
上面說完了MyBatis,接著看一下MyBatis二級緩存,還是從二級緩存工作流程開始。還是從DefaultSqlSession的selectList方法進去:
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); } catch (Exception e) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
執行query方法,方法位於CachingExecutor中:
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); }
繼續跟第4行的query方法,同樣位於CachingExecutor中:
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 and #116 } return list; } } return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }
從這裡看到,執行第17行的BaseExecutor的query方法之前,會先拿Mybatis二級緩存,而BaseExecutor的query方法會優先讀取MyBatis一級緩存,由此可以得出一個重要結論:假如定義了MyBatis二級緩存,那麼MyBatis二級緩存讀取優先級高於MyBatis一級緩存。
而第3行~第16行的邏輯:
第5行的方法很好理解,根據flushCache=true或者flushCache=false判斷是否要清理二級緩存
第7行的方法是保證MyBatis二級緩存不會存儲存儲過程的結果
第9行的方法先嘗試從tcm中獲取查詢結果,這個tcm解釋一下,這又是一個裝飾器模式(數數MyBatis用到了多少裝飾器模式了),創建一個事物緩存TranactionalCache,持有Cache接口,Cache接口的實現類就是根據我們在Mapper文件中配置的<cache>創建的Cache實例
第10行~第12行,如果沒有從MyBatis二級緩存中拿到數據,那麼就會查一次數據庫,然後放到MyBatis二級緩存中去
至於如何判定上次查詢和這次查詢是一次查詢?由於這裡的CacheKey和MyBatis一級緩存使用的是同一個CacheKey,因此它的判定條件和前文寫過的MyBatis一級緩存三個維度的判定條件是一致的。
最後再來談一點,"Cache cache = ms.getCache()"這句代碼十分重要,這意味著Cache是從MappedStatement中獲取到的,而MappedStatement又和每一個<insert>、<delete>、<update>、<select>綁定並在MyBatis啟動的時候存入Configuration中:
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection");
因此MyBatis二級緩存的生命周期即整個應用的生命周期,應用不結束,定義的二級緩存都會存在在內存中。
從這個角度考慮,為了避免MyBatis二級緩存中數據量過大導致內存溢出,MyBatis在配置文件中給我們增加了很多配置例如size(緩存大小)、flushInterval(緩存清理時間間隔)、eviction(數據淘汰算法)來保證緩存中存儲的數據不至於太過龐大。
MyBatis二級緩存實例化過程
接著看一下MyBatis二級緩存<cache>實例化的過程,代碼位於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); } }
這裡分別取<cache>中配置的各個屬性,關注一下兩個默認值:
type表示緩存實現,默認是PERPETUAL,根據typeAliasRegistry中注冊的,PERPETUAL實際對應PerpetualCache,這和MyBatis一級緩存是一致的
eviction表示淘汰算法,默認是LRU算法
第3行~第11行拿到了所有屬性,那麼調用12行的useNewCache方法創建緩存:
public Cache useNewCache(Class<? extends Cache> typeClass, Class<? extends Cache> evictionClass, Long flushInterval, Integer size, boolean readWrite, boolean blocking, Properties props) { 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(); configuration.addCache(cache); currentCache = cache; return cache; }
這裡又使用了建造者模式,跟一下第16行的build()方法,在此之前該傳入的參數都已經傳入了CacheBuilder:
public Cache build() { setDefaultImplementations(); Cache cache = newBaseCacheInstance(implementation, id); setCacheProperties(cache); // issue #352, do not apply decorators to custom caches if (PerpetualCache.class.equals(cache.getClass())) { for (Class<? extends Cache> decorator : decorators) { cache = newCacheDecoratorInstance(decorator, cache); setCacheProperties(cache); } cache = setStandardDecorators(cache); } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) { cache = new LoggingCache(cache); } return cache; }
第3行的代碼,構建基礎的緩存,implementation指的是type配置的值,這裡是默認的PerpetualCache。
第6行的代碼,如果是PerpetualCache,那麼繼續裝飾(又是裝飾器模式,可以數數這幾篇MyBatis源碼解析的文章裡面出現了多少次裝飾器模式了),這裡的裝飾是根據eviction進行裝飾,到這一步,給PerpetualCache加上了LRU的功能。
第11行的代碼,繼續裝飾,這次MyBatis將它命名為標准裝飾,setStandardDecorators方法實現為:
private Cache setStandardDecorators(Cache cache) { try { MetaObject metaCache = SystemMetaObject.forObject(cache); if (size != null && metaCache.hasSetter("size")) { metaCache.setValue("size", size); } if (clearInterval != null) { cache = new ScheduledCache(cache); ((ScheduledCache) cache).setClearInterval(clearInterval); } if (readWrite) { cache = new SerializedCache(cache); } cache = new LoggingCache(cache); cache = new SynchronizedCache(cache); if (blocking) { cache = new BlockingCache(cache); } return cache; } catch (Exception e) { throw new CacheException("Error building standard cache decorators. Cause: " + e, e); } }
這次是根據其它的配置參數來:
如果配置了flushInterval,那麼繼續裝飾為ScheduledCache,這意味著在調用Cache的getSize、putObject、getObject、removeObject四個方法的時候都會進行一次時間判斷,如果到了指定的清理緩存時間間隔,那麼就會將當前緩存清空 如果readWrite=true,那麼繼續裝飾為SerializedCache,這意味著緩存中所有存儲的內存都必須實現Serializable接口 跟配置無關,將之前裝飾好的Cache繼續裝飾為LoggingCache與SynchronizedCache,前者在getObject的時候會打印緩存命中率,後者將Cache接口中所有的方法都加了Synchronized關鍵字進行了同步處理 如果blocking=true,那麼繼續裝飾為BlockingCache,這意味著針對同一個CacheKey,拿數據與放數據、刪數據是互斥的,即拿數據的時候必須沒有在放數據、刪數據Cache全部裝飾完畢,返回,至此MyBatis二級緩存生成完畢。
最後說一下,MyBatis支持三種類型的二級緩存:
MyBatis默認的緩存,type為空,Cache為PerpetualCache 自定義緩存 第三方緩存從build()方法來看,後兩種場景的Cache,MyBatis只會將其裝飾為LoggingCache,理由很簡單,這些緩存的定期清除功能、淘汰過期數據功能開發者自己或者第三方緩存都已經實現好了,根本不需要依賴MyBatis本身的裝飾。