Mybatis 與 Hibernate 同樣,支持一二級緩存。一級緩存指的是 Session 級別的緩存,即在一個會話中屢次執行同一條 SQL 語句而且參數相同,則後面的查詢將不會發送到數據庫,直接從 Session 緩存中獲取。二級緩存,指的是 SessionFactory 級別的緩存,即不一樣的會話能夠共享。java
緩存,一般涉及到緩存的寫、讀、過時(更新緩存)等幾個方面,請帶着這些問題一塊兒來探究Mybatis關於緩存的實現原理吧。算法
提出問題:緩存的查詢順序,是先查一級緩存仍是二級緩存?數據庫
本文以 SQL 查詢與更新兩個流程來揭開 Mybatis 緩存實現的細節。緩存
舒適提示,建議在閱讀本文以前先閱讀筆者的另外幾篇文章:
1)源碼分析Mybatis MapperProxy初始化【圖文並茂】
2)源碼分析Mybatis MappedStatement的建立流程
3)【圖文並茂】源碼解析MyBatis Sharding-Jdbc SQL語句執行流程詳解
4)【圖文並茂】Mybatis執行SQL的4大基礎組件詳解微信
舒適提示,本文不會詳細介紹詳細的 SQL 執行流程,若是對其感興趣,能夠查閱筆者的另一篇文章:【圖文並茂】源碼解析MyBatis Sharding-Jdbc SQL語句執行流程詳解mybatis
具體實現由 Configuration 的 newExecutor 方法實現。app
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); } if (cacheEnabled) { // @1 executor = new CachingExecutor(executor); // @2 } executor = (Executor) interceptorChain.pluginAll(executor); return executor; }
代碼@1:若是 cacheEnabled 爲 true,表示開啓緩存機制,緩存的實現類爲 CachingExecutor,這裏使用了經典的裝飾模式,處理了緩存的相關邏輯後,委託給的具體的 Executor 執行。框架
cacheEnable 在實際的使用中經過在 mybatis-config.xml 文件中指定,例如:ide
<configuration> <settings> <setting name="cacheEnabled" value="true"> </settings> </configuration>
該值默認爲true。源碼分析
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameterObject); // @1 CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql); // @2 return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); // @3 }
代碼@1:根據參數生成SQL語句。
代碼@2:根據 MappedStatement、參數、分頁參數、SQL 生成緩存 Key。
代碼@3:調用6個參數的 query 方法。
緩存 Key 的建立比較簡單,本文就只貼出代碼,你們一目瞭然,你們重點關注組成緩存Key的要素。
BaseExecute#createCacheKey
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) { if (closed) { throw new ExecutorException("Executor was closed."); } CacheKey cacheKey = new CacheKey(); cacheKey.update(ms.getId()); cacheKey.update(rowBounds.getOffset()); cacheKey.update(rowBounds.getLimit()); cacheKey.update(boundSql.getSql()); List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry(); // mimic DefaultParameterHandler logic for (ParameterMapping parameterMapping : parameterMappings) { if (parameterMapping.getMode() != ParameterMode.OUT) { Object value; String propertyName = parameterMapping.getProperty(); 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 = configuration.newMetaObject(parameterObject); value = metaObject.getValue(propertyName); } cacheKey.update(value); } } if (configuration.getEnvironment() != null) { // issue #176 cacheKey.update(configuration.getEnvironment().getId()); } return cacheKey; }
接下來重點看CachingExecutor的另一個query方法。
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(); // @1 if (cache != null) { flushCacheIfRequired(ms); // @2 if (ms.isUseCache() && resultHandler == null) { ensureNoOutParams(ms, boundSql); @SuppressWarnings("unchecked") List<E> list = (List<E>) tcm.getObject(cache, key); // @3 if (list == null) { // @4 list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); //@5 tcm.putObject(cache, key, list); // issue #578 and #116 // @6 } return list; } } return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); //@7 }
代碼@1:獲取 MappedStatement 中的 Cache cache 屬性。
代碼@2:若是不爲空,則嘗試從緩存中獲取,不然直接委託給具體的執行器執行,例如 SimpleExecutor (@7)。
代碼@3:嘗試從緩存中根據緩存 Key 查找。
代碼@4:若是從緩存中獲取的值不爲空,則直接返回緩存中的值,不然先從數據庫查詢@5,將查詢結果更新到緩存中。
這裏的緩存即 MappedStatement 中的 Cache 對象是一級緩存仍是二級緩存?一般在 ORM 類框架中,Session 級別的緩存爲一級緩存,即會話結束後就會失效,顯然這裏不會隨着 Session 的失效而失效,由於 Cache 對象是存儲在於 MappedStatement 對象中的,每個 MappedStatement 對象表明一個 Dao(Mapper) 中的一個方法,即表明一條對應的 SQL 語句,是一個全局的概念。
相信你們也會以爲,想繼續深刻了解 CachingExecutor 中使用的 Cache 是一級緩存仍是二級緩存,瞭解 Cache 對象的建立相當重要。關於 MappedStatement 的建立流程,建議查閱筆者的另一篇博文:源碼分析Mybatis MappedStatement的建立流程。
本文只會關注 MappedStatement 對象流程中關於緩存相關的部分。
接下來將按照先二級緩存,再一級緩存的思路進行講解。
從上面看,若是 cacheEnable 爲 true 而且 MappedStatement 對象的 cache 屬性不爲空,則能使用二級緩存。
咱們能夠看到 MappedStatement 對象的 cache 屬性賦值的地方爲:MapperBuilderAssistant 的 addMappedStatement 方法,從該方法的調用鏈能夠得知是在解析 Mapper 定義的時候就會建立。
在這裏插入圖片描述
使用的 cache 屬性爲 MapperBuilderAssistant 的 currentCache,咱們跟蹤一下該屬性的賦值方法:
public Cache useCacheRef(String namespace)
其調用鏈以下:
在這裏插入圖片描述
能夠看出是在解析 cacheRef 標籤,即在解析 Mapper.xml 文件中的 cacheRef 標籤時,即二級緩存的使用和 cacheRef 標籤離不開關係,而且特別注意一點,其參數爲 namespace,即每個 namespace 對應一個 Cache 對象,在 Mybatis 的方法中,一般namespace 對一個 Mapper.java 對象,對應對數據庫一張表的更新、新增操做。
public Cache useNewCache
其調用鏈以下圖所示:
在這裏插入圖片描述
在解析 Mapper.xml 文件中的 cache 標籤時被調用。
接下來咱們根據 cache 標籤簡單看一下 cache 標籤的解析,下面以 xml 配置方式爲例展開,基於註解的解析,其原理相似,其代碼 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 標籤的核心屬性以下:
在這裏插入圖片描述
cacheRef 只有一個屬性,就是 namespace,就是引用其餘 namespace 中的 cache。
Cache 的建立流程就講解到這裏,同一個 Namespace 只會定義一個 Cache。二級緩存的建立是在 *Mapper.xml 文件中使用了< cache/>、< cacheRef/>標籤時建立,而且會按 NameSpace 爲維度,爲各個 MapperStatement 傳入它所屬的 Namespace 的二級緩存對象。
二級緩存的查詢邏輯就介紹到這裏了,咱們再次回看 CacheingExecutor 的查詢方法:
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(); // @1 if (cache != null) { flushCacheIfRequired(ms); // @2 if (ms.isUseCache() && resultHandler == null) { ensureNoOutParams(ms, boundSql); @SuppressWarnings("unchecked") List<E> list = (List<E>) tcm.getObject(cache, key); // @3 if (list == null) { // @4 list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); //@5 tcm.putObject(cache, key, list); // issue #578 and #116 // @6 } return list; } } return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); //@7 }
若是 MappedStatement 的 cache 屬性爲空,則直接調用內部的 Executor 的查詢方法。也就是若是在 *.Mapper.xm l文件中未定義< cache/>或< cacheRef/>,則 cache 屬性會爲空。
Mybatis 根據 SQL 的類型共有以下3種 Executor類型,分別是 SIMPLE, REUSE, BATCH,本文將以 SimpleExecutor爲 例來對一級緩存的介紹。
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()) { // @1 clearLocalCache(); } List<E> list; try { queryStack++; list = resultHandler == null ? (List<E>) localCache.getObject(key) : null; // @2 if (list != null) { handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); } else { list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); // @3 } } finally { queryStack--; } if (queryStack == 0) { for (DeferredLoad deferredLoad : deferredLoads) { deferredLoad.load(); } // issue #601 deferredLoads.clear(); if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { // issue #482 clearLocalCache(); } } return list; }
代碼@1:queryStack:查詢棧,每次查詢以前,加一,查詢返回結果後減一,若是爲1,表示整個會會話中沒有執行的查詢語句,並根據 MappedStatement 是否須要執行清除緩存,若是是查詢類的請求,無需清除緩存,若是是更新類操做的MappedStatemt,每次執行以前都須要清除緩存。
代碼@2:若是緩存中存在,直接返回緩存中的數據。
代碼@3:若是緩存未命中,則調用 queryFromDatabase 從數據中查詢。
咱們順便看一下 queryFromDatabase 方法,再來看一下一級緩存的實現類。
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { List<E> list; localCache.putObject(key, EXECUTION_PLACEHOLDER); //@! try { list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); // @2 } finally { localCache.removeObject(key); // @3 } localCache.putObject(key, list); // @4 if (ms.getStatementType() == StatementType.CALLABLE) { localOutputParameterCache.putObject(key, parameter); } return list; }
代碼@1:先往本地緩存存入一個特定值,表示正在執行中。
代碼@2:從數據中查詢數據。
代碼@3:先移除正在執行中的標記。
代碼@4:將數據庫中的值存儲到一級緩存中。
能夠看出一級緩存的屬性爲 localCache,爲 Executor 的屬性。若是你們看過筆者發佈的這個 Mybatis 系列就能輕易得出一個結論,每個 SQL 會話對應一個 SqlSession 對象,每個 SqlSession 會對應一個 Executor 對象,故 Executor 級別的緩存即爲Session 級別的緩存,即爲 Mybatis 的一級緩存。
上面已經介紹了一二級緩存的查找與添加,在查詢的時候,首先查詢緩存,若是緩存未命中,則查詢數據庫,而後將查詢到的結果存入緩存中。
下面咱們來簡單看看緩存的更新。
從更新的角度,更加的是關注緩存的更新,即當數據發生變化後,若是清除對應的緩存。
CachingExecutor#update
public int update(MappedStatement ms, Object parameterObject) throws SQLException { flushCacheIfRequired(ms); // @1 return delegate.update(ms, parameterObject); // @2 }
代碼@1:若是有必要則刷新緩存。
代碼@2:調用內部的 Executor,例如 SimpleExecutor。
接下來重點看一下 flushCacheIfRequired 方法。
private void flushCacheIfRequired(MappedStatement ms) { Cache cache = ms.getCache(); if (cache != null && ms.isFlushCacheRequired()) { tcm.clear(cache); } } TransactionalCacheManager#clear public void clear(Cache cache) { getTransactionalCache(cache).clear(); }
TransactionalCacheManager 事務緩存管理器,其實就是對 MappedStatement 的 cache 屬性進行裝飾,最終調用的仍是MappedStatement 的 getCache 方法獲得其緩存對象而後調用 clear 方法,清空全部的緩存,即緩存的更新策略是隻要namespace 的任何一條插入或更新語句執行,整個 namespace 的緩存數據將所有清空。
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); }
其更新策略與二級緩存維護的同樣。
一二級緩存的的新增、查詢、更新就介紹到這裏了,接下來對其進行一個總結。
Mybatis 一二級緩存時序圖以下:
在這裏插入圖片描述
3.2 如何使用二級緩存
一、在mybatis-config.xml中將cacheEnable設置爲true。例如:
<configuration> <settings> <setting name="cacheEnabled" value="true"> </settings> </configuration>
不過該值默認爲true。
二、在須要緩存的表操做,對應的 Dao 的配置文件中,例如 *Mapper.xml 文件中使用 cache、或 cacheRef 標籤來定義緩存。
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="com.winterchen.dao.UserDao" > <insert id="insert" parameterType="com.winterchen.model.UserDomain"> //省略 </insert> <select id="selectUsers" resultType="com.winterchen.model.UserDomain"> //省略 </select> <cache type="lru" readOnly="true" flushInterval="3600000"></cache> </mapper>
這樣就定義了一個 Cache,其 namespace 爲 com.winterchen.dao.UserDao。其中 flushInterval 定義該 cache 定時清除的時間間隔,單位爲 ms。
若是一個表的更新操做、新增操做位於不一樣的 Mapper.xml 文件中,若是對一個表的操做的 Cache 定義在不一樣的文件,則緩存數據則會出現不一致的狀況,由於 Cache 的更新邏輯是,在一個 Namespace 中,若是有更新、插入語句的執行,則會清除該 namespace 對應的 cache 裏面的全部緩存。那怎麼來處理這種場景呢?cacheRef 閃亮登場。
若是一個 Mapper.xml 文件須要引入定義在別的 Mapper.xml 文件中定義的 cache,則使用 cacheRef,示例以下:
<cacheRef "namespace" = "com.winterchen.dao.UserDao"/>
一級緩存默認是開啓的,也沒法關閉。
緩存的介紹就介紹到這裏。若是本文對您有所幫助,麻煩點一下贊,謝謝。
更多文章請關注微信公衆號: