頻繁的數據庫操作是非常耗費性能的(主要是因為對於DB而言,數據是持久化在磁盤中的,因此查詢操作需要通過IO,IO操作速度相比內存操作速度慢了好幾個量級),尤其是對於一些相同的查詢語句,完全可以把查詢結果存儲起來,下次查詢同樣的內容的時候直接從內存中獲取數據即可,這樣在某些場景下可以大大提升查詢效率。
-
MyBatis的緩存分為兩種:
- 一級緩存,一級緩存是SqlSession級別的緩存,對於相同的查詢,會從緩存中返回結果而不是查詢數據庫
- 二級緩存,二級緩存是Mapper級別的緩存,定義在Mapper文件的<cache>標簽中並需要開啟此緩存,多個Mapper文件可以共用一個緩存,依賴<cache-ref>標簽配置
SqlSession>DefaultSqlSession(selectList)>this.executor.query>Executor(一級緩存BaseExecutor, 二級緩存CachingExecutor)
CacheKey判斷兩次查詢條件是否一致。
一級緩存:/executor/BaseExecutor.class
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(this.closed) { throw new ExecutorException("Executor was closed."); } else { if(this.queryStack == 0 && ms.isFlushCacheRequired()) { this.clearLocalCache(); } List list; try { ++this.queryStack; list = resultHandler == null?(List)this.localCache.getObject(key):null; if(list != null) { this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); } else { list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); } } finally { --this.queryStack; } if(this.queryStack == 0) { Iterator i$ = this.deferredLoads.iterator(); while(i$.hasNext()) { BaseExecutor.DeferredLoad deferredLoad = (BaseExecutor.DeferredLoad)i$.next(); deferredLoad.load(); } this.deferredLoads.clear(); if(this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { this.clearLocalCache(); } } return list; } }
一級緩存三個結論:
- 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。增刪改沒有一級緩存。
//緩存判斷查詢條件是否一致:
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) { if(this.closed) { throw new ExecutorException("Executor was closed."); } else { CacheKey cacheKey = new CacheKey(); cacheKey.update(ms.getId());//判斷id屬性是否相同 cacheKey.update(Integer.valueOf(rowBounds.getOffset()));//判斷Offset屬性是否相同 cacheKey.update(Integer.valueOf(rowBounds.getLimit()));//判斷Limit屬性是否相同 cacheKey.update(boundSql.getSql());//判斷sql屬性是否相同 List parameterMappings = boundSql.getParameterMappings();//后面都是判斷參數是否相同 TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry(); for(int i = 0; i < parameterMappings.size(); ++i) { ParameterMapping parameterMapping = (ParameterMapping)parameterMappings.get(i); if(parameterMapping.getMode() != ParameterMode.OUT) { String propertyName = parameterMapping.getProperty(); Object value; if(boundSql.hasAdditionalParameter(propertyName)) { value = boundSql.getAdditionalParameter(propertyName); } else if(parameterObject == null) { value = null; } else if(typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { value = parameterObject; } else { MetaObject metaObject = this.configuration.newMetaObject(parameterObject); value = metaObject.getValue(propertyName); } cacheKey.update(value); } } return cacheKey; } }
一級緩存從四組共五個條件判斷兩次查詢是相同的:
- <select>標簽所在的Mapper的Namespace+<select>標簽的id屬性
- RowBounds的offset和limit屬性,RowBounds是MyBatis用於處理分頁的一個類,offset默認為0,limit默認為Integer.MAX_VALUE
- <select>標簽中定義的sql語句
- 輸入參數的具體參數值,一個int值就update一個int,一個String值就update一個String,一個List就輪詢里面的每個元素進行update
即只要兩次查詢滿足以上三個條件且沒有定義flushCache="true",那么第二次查詢會直接從MyBatis一級緩存PerpetualCache中返回數據,而不會走DB。
MyBatis二級緩存的生命周期即整個應用的生命周期,應用不結束,定義的二級緩存都會存在在內存中。
從這個角度考慮,為了避免MyBatis二級緩存中數據量過大導致內存溢出,MyBatis在配置文件中給我們增加了很多配置例如size(緩存大小)、flushInterval(緩存清理時間間隔)、eviction(數據淘汰算法)來保證緩存中存儲的數據不至於太過龐大。
二級緩存:/executor/CachingExecutor.class
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { Cache cache = ms.getCache();//Cache是從MappedStatement中獲取到的,而MappedStatement又和每一個<insert>、<delete>、<update>、<select>綁定並在MyBatis啟動的時候存入Configuration中: if(cache != null) { this.flushCacheIfRequired(ms);//根據flushCache=true或者flushCache=false判斷是否要清理二級緩存 if(ms.isUseCache() && resultHandler == null) { this.ensureNoOutParams(ms, parameterObject, boundSql);//保證MyBatis二級緩存不會存儲存儲過程的結果 List list = (List)this.tcm.getObject(cache, key);//tcm裝飾器模式,創建一個事物緩存TranactionalCache,持有Cache接口,Cache接口的實現類就是根據我們在Mapper文件中配置的<cache>創建的Cache實例 if(list == null) { list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); this.tcm.putObject(cache, key, list); }//如果沒有從MyBatis二級緩存中拿到數據,那么就會查一次數據庫,然后放到MyBatis二級緩存中去 return list; } }
//優先讀取以上二級緩存,query方法優先讀取默認實現的一級緩存。
return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
MyBatis支持三種類型的二級緩存:
- MyBatis默認的緩存,type為空,Cache為PerpetualCache
- 自定義緩存
- 第三方緩存
<settings>//mybatis.cfg.xml <!-- 開啟二級緩存 默認值為true --> <setting name="cacheEnabled" value="true"/> </settings> <mapper namespace="cn.sxt.vo.user.mapper">//mapper.xml <!-- 開啟本mapper namespace下的二級緩存 --> <cache></cache>
select a.col1, a.col2, a.col3, b.col1, b.col2, b.col3 from tableA a, tableB b where a.id = b.id;
對於tableA與tableB的操作定義在兩個Mapper中,分別叫做MapperA與MapperB,即它們屬於兩個命名空間,如果此時啟用緩存:
- MapperA中執行上述sql語句查詢這6個字段
- tableB更新了col1與col2兩個字段
- MapperA再次執行上述sql語句查詢這6個字段(前提是沒有執行過任何insert、delete、update操作)
此時問題就來了,即使第(2)步tableB更新了col1與col2兩個字段,第(3)步MapperA走二級緩存查詢到的這6個字段依然是原來的這6個字段的值,因為我們從CacheKey的3組條件來看:
- <select>標簽所在的Mapper的Namespace+<select>標簽的id屬性
- RowBounds的offset和limit屬性,RowBounds是MyBatis用於處理分頁的一個類,offset默認為0,limit默認為Integer.MAX_VALUE
- <select>標簽中定義的sql語句
對於MapperA來說,其中的任何一個條件都沒有變化,自然會將原結果返回。
這個問題對於MyBatis的二級緩存來說是一個無解的問題,因此使用MyBatis二級緩存有一個前提:
必須保證所有的增刪改查都在同一個命名空間下才行。