我們都知道mybatis有一級,二級緩存之說。我們對他們的了解大部分都是這樣:1,一級緩存對應SqlSession,二級緩存對應mapper。2,一級緩存默認開啟,二級緩存需要手動開啟3,當查詢的時候,在二級緩存開...
我們都知道mybatis有一級,二級緩存之說。我們對他們的了解大部分都是這樣:
1,一級緩存對應SqlSession,二級緩存對應mapper。
2,一級緩存默認開啟,二級緩存需要手動開啟
3,當查詢的時候,在二級緩存開啟的情況下查二級緩存,先查二級緩存,如果二級緩存也沒有,然后查一級緩存,如果一級緩存再沒有,則查數據庫。查完數據庫之后把數據放到緩存中。
那么,今天我們通過源碼的方式來了解一下mybatis的緩存原理。
1,當我們走mapper1.selectOne(map);這行代碼的時候:
會通過代理的方式會走到下面第5代碼executor.query()。
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { try { MappedStatement ms = configuration.getMappedStatement(statement); //會先調用CachingExecutor子類的query 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(); }}
我們就以這個executor作為切入點進入觀察,跟進斷點可知:這個executor會先使用他的子類CachingExecutor調用query(),這個類一看名字就知道跟緩存相關的。
我們來看這個CachingExecutor的query()方法:
@Overridepublic <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { //處理sql語句 BoundSql boundSql = ms.getBoundSql(parameterObject); //創建緩存key CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql); //通過這個key作為緩存進行調用query方法 return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}
這里其中如何創建這個CacheKey等會再說,我們先看看query()
@Overridepublic <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { // ms.getCache()拿到緩存對象 開啟二級緩存菜回進入if里面 Cache cache = ms.getCache(); if (cache != null) { flushCacheIfRequired(ms); //如果標簽中設置useCache=false,那么不使用二級緩存 if (ms.isUseCache() && resultHandler == null) { ensureNoOutParams(ms, boundSql); @SuppressWarnings("unchecked") List<E> list = (List<E>) tcm.getObject(cache, key); if (list == null) { list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); tcm.putObject(cache, key, list); // issue #578 and #116 } return list; } } //如果不開啟直接走下面這條查詢,其實這條查詢跟13行是一樣的。 return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}
這個MappedStatement.getCache()就是我們說的二級緩存。同時它會判斷我們的二級緩存是否開啟ms.isUseCache()。
那么它是如何初始化二級緩存并且如何設置isUseCache的呢?
這就回到我們解析配置文件說起(之前寫過一次mybatis的執行流程,不清楚可以去看看,這里只是略講):
1,在解析的時候會先后調用兩個方法,如圖:
在settingsElement方法中,默認對mybatis配置文件是否啟用二級緩存功能設置true。有人就奇怪了,二級緩存不是默認不開啟嗎,怎么默認為true?不要著急。。
然后是mapperElement方法中,看調用的mapperParser.parse()的方法
我們在點進去看configurationElement這個方法:
這個cacheElement()方法就是解析我們映射文件是否配置了<cache/>
如果不配置,那么什么也不做。也就是整個mapper中不開啟二級緩存!
這我們就說明了Cache cache = ms.getCache();的由來
那么還有一個問題就是這個判斷ms.isUseCache(),這個有什么用呢?也就是mapper雖然開啟了二級緩存,它還要看看,select標簽中是否禁用了二級緩存,默認如果不配置,那么為true,以下圖中代碼。
而如果配置為false了,將不會走
if (ms.isUseCache() && resultHandler == null){ //.......}
這個分支的代碼,也就是不使用二級緩存。
講到這里,大家應該明白為何二級緩存需要手動開啟了吧?默認情況下,mybatis配置文件開啟二級緩存,而mapper映射文件默認是關閉二級緩存的,簡單的說配置文件開啟了,可是我映射文件沒有開啟,那么還是不開啟,所以我們說的開啟二級緩存其實就是在映射文件中開啟。當我們在映射文件中開啟了二級緩存,那么映射文件中的select標簽也是默認開啟二級緩存的,我們可以通過這種機制來使某個select禁用二級緩存(在select標簽中加入useCache=false即可)。
2)繼續說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, boundSql); @SuppressWarnings("unchecked") List<E> list = (List<E>) tcm.getObject(cache, key); if (list == null) { list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); tcm.putObject(cache, key, list); // issue #578 and #116 } return list; } } return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}
如果開啟二級緩存,則直接當我們進入二級緩存的邏輯之中,tcm.getObject(cache, key)這個就是根據key從緩存中拿,如果拿不到,則調用這個delegate.query()方法拿。
if (ms.isUseCache() && resultHandler == null) { ensureNoOutParams(ms, boundSql); @SuppressWarnings("unchecked") //從緩存中拿數據 List<E> list = (List<E>) tcm.getObject(cache, key); //判斷是否拿到,如果拿不到則delegate.query( if (list == null) { list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); tcm.putObject(cache, key, list); // issue #578 and #116 } return list;}
以上就是mybatis二級緩存中獲取數據的邏輯
我們再來看看delegate.query()這個方法:
通過代碼我們發現,如果沒有開啟二級緩存,或者二級緩存中沒有數據
那么最后都是執行delegate.query()方法。
這個方法由Executor的子類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++; //同樣,先從緩存中拿 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--; } //.... return list;}
這個代碼就很好了,跟之前差不多,13行就是從緩存中拿,如果拿不到,那么就直接從18行中從數據庫中拿。這就是從一級緩存中拿數據的流程。
這就是mybatis查詢語句從緩存獲取數據的流程。
那么這個緩存對象到底是什么??
我們可以開啟二級緩存,然后通過CachingExecutor的query()打斷點進入看看Cache cache = ms.getCache();這個是什么。
這樣,我們就能找到LruCache這個Cache了。
這個LruCache有個成員變量:private final Cache delegate;我們可以通過之前看出他就是一個PerpetualCache類,
到這里就不難看出,不管是一級緩存還是二級緩存,他們的緩存設計思想都是Map。
既然我們懂了緩存其實就是個map,那么如何確定key呢??
這就是之前沒有說的CacheKey,也就是下面代碼的第5行
@Overridepublic <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameterObject); // 確定key,如何確定? CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql); return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}
createCacheKey同樣是由BaseExecutor進行調用createCacheKey()方法。
我們最后再看看它的實現邏輯:
它就是通過一系列的update語句來確認CacheKey,具體如下:
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) { if (closed) { throw new ExecutorException("Executor was closed."); } CacheKey cacheKey = new CacheKey(); //確定cacheKey 的邏輯如下 cacheKey.update(ms.getId());//通過mapper中的select標簽id cacheKey.update(rowBounds.getOffset());//跟分頁有關,不用理 cacheKey.update(rowBounds.getLimit());//跟分頁有關,不用理 cacheKey.update(boundSql.getSql());//通過sql語句 //省略代碼 return cacheKey;}
那么update方法中到底如何實現?
public void update(Object object) { int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object); count++; checksum += baseHashCode; baseHashCode *= count; hashcode = multiplier * hashcode + baseHashCode; updateList.add(object);}
看到HashCode就明白了update就是通過哈希算法來確認CacheKey的。
我們明白了select的過程以及使用緩存的原理,那么update呢?其實好多人都知道mybatis在執行update,insert,delete之后,都會清空緩存。
因為在執行update,insert,delete的過程中,最后都是BaseExecutor調用update方法,所以我們只要看這個代碼
這個就是清空緩存的代碼。
以上就是原理,最后我們通過select執行打印的sql的例子來看看效果:
1,關閉二級緩存:
效果:兩次一樣的查詢只發出一條sql
2,開啟二級緩存:
結果:同樣只發出一條sql
總結:
1,二級緩存需要手動開啟,其實就是只用在映射文件中加入<cache/>。因為mybatis配置文件已經默認開啟了二級緩存。
2,發出select語句之后,如果開啟二級緩存,先在二級緩存中查數據。如果查不到數據,則查一級緩存,如果查不到,則最后查數據庫。
3,insert,update,delete執行后,會清空緩存,不管是一級還是二級緩存
4,緩存對象Cache其實就是Map來實現的
5,緩存的key分別由select標簽id,分頁信息,sql通過一系列哈希算法來確定
最后附上Mybatis緩存執行流程:
本人水平有限,難免有錯誤或遺漏之處,望大家指正和諒解,提出寶貴意見,愿與之交流。
來源:本文內容搜集或轉自各大網絡平臺,并已注明來源、出處,如果轉載侵犯您的版權或非授權發布,請聯系小編,我們會及時審核處理。
聲明:江蘇教育黃頁對文中觀點保持中立,對所包含內容的準確性、可靠性或者完整性不提供任何明示或暗示的保證,不對文章觀點負責,僅作分享之用,文章版權及插圖屬于原作者。
Copyright?2013-2024 JSedu114 All Rights Reserved. 江蘇教育信息綜合發布查詢平臺保留所有權利
蘇公網安備32010402000125
蘇ICP備14051488號-3技術支持:南京博盛藍睿網絡科技有限公司
南京思必達教育科技有限公司版權所有 百度統計