Mybatis CachingExecutor二級(jí)緩存使用示例詳解
前言
上次我們講Mybatis的緩存時(shí),我們提到了CachingExecutor,知道了這個(gè)帶緩存的執(zhí)行器就是二級(jí)緩存的來(lái)源,這次我們系統(tǒng)的分析下其是如何產(chǎn)生作用的
一、CachingExecutor的在邏輯定位
流程圖中的位置
我把CachingExecutor在邏輯鏈路中的位置標(biāo)出來(lái)了,就是儲(chǔ)存在會(huì)話對(duì)象中,通過會(huì)話可使用到CachingExecutor,而CachingExecutor又內(nèi)置一個(gè)SimpleExecutor,熟悉設(shè)計(jì)模式的同學(xué)應(yīng)該知道這就是所謂的委派模式。當(dāng)然,這里面會(huì)話內(nèi)置的也可能直接就是SimpleExecutor了,那樣的話,調(diào)用的就直接是SimpleExecutor執(zhí)行器了。
二、CachingExecutor的生效
既然知道了CachingExecutor在會(huì)話對(duì)象中,那毫無(wú)疑問,就是在創(chuàng)建會(huì)話的時(shí)候,把一個(gè)CachingExecutor放入到會(huì)話對(duì)象中的,我們來(lái)看看,要實(shí)現(xiàn)這個(gè)目標(biāo)要做什么
1.全局參數(shù)
首先,要想啟用CachingExecutor,我們得開啟一個(gè)全局的設(shè)置參數(shù)
mybatis.configuration.cache-enabled=true
為什么開了這個(gè)參數(shù)就有用呢?其實(shí)不難猜想,其作用的位置肯定還是在創(chuàng)建會(huì)話對(duì)象的時(shí)候,我們直接看源碼吧
SqlSessionUtils.class
// SqlSessionUtils.class // 通過工廠對(duì)象(單例,存在容器中),開啟會(huì)話,獲得會(huì)話對(duì)象,executorType是執(zhí)行器枚舉類(SIMPLE, REUSE, BATCH),一般是SIMPLE session = sessionFactory.openSession(executorType);
DefaultSqlSessionFactory.class
// DefaultSqlSessionFactory.class private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx = null; try { final Environment environment = configuration.getEnvironment(); final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); // 通過configuration(Mybatis配置,單例)創(chuàng)建執(zhí)行器 final Executor executor = configuration.newExecutor(tx, execType); return new DefaultSqlSession(configuration, executor, autoCommit); } catch (Exception e) { closeTransaction(tx); // may have fetched a connection so lets call close() throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
Configuration.class
// Configuration.class 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 { // 創(chuàng)建了一個(gè)簡(jiǎn)易執(zhí)行器 executor = new SimpleExecutor(this, transaction); } // 如果開啟了緩存,即 mybatis.configuration.cache-enabled=true if (cacheEnabled) { // 創(chuàng)建了緩存執(zhí)行器,并且把簡(jiǎn)易執(zhí)行器作為其 delegate,通過構(gòu)造方法放入 executor = new CachingExecutor(executor); } executor = (Executor) interceptorChain.pluginAll(executor); return executor; }
如上,可以看出CachingExecutor就是由 mybatis.configuration.cache-enabled=true 開啟的,并且和會(huì)話的情況無(wú)關(guān),這是一個(gè)全局設(shè)置,一旦開啟,所有會(huì)話都會(huì)首先調(diào)用CachingExecutor
2. MappedStatement啟用Cache
是不是我們啟用了CachingExecutor就可以使用二級(jí)緩存了呢?為什么這么說,我們還是直接看CachingExecutor的源碼
CachingExecutor.class
// CachingExecutor.class @Override public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { // mappedStatement 就是我們寫的mapper接口里的某個(gè)方法的所有信息,注意其描述的是一個(gè)方法,而不是一整個(gè)mapper接口的所有方法 // 當(dāng)然,它同樣也包含某些mapper層次的設(shè)置,如對(duì)應(yīng)的xml文件位置等,此處的Cache同樣是mapper層次的設(shè)置 Cache cache = ms.getCache(); // 有Cache才會(huì)真正的去找二級(jí)緩存,否則直接就讓委托的執(zhí)行器去查詢數(shù)據(jù)了。 // 注意此處的Cache并不是緩存本身,而是mapper里的緩存配置 if (cache != null) { // .... List<E> list = (List<E>) tcm.getObject(cache, key); // .... return list; } } return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }
可以看到,要想真正啟用緩存,還得在Mapper層級(jí)進(jìn)行一次緩存配置,也就是所謂的
聲明下Cache時(shí)可以設(shè)置一下參數(shù)
<cache eviction="FIFO" flushinterval="60000" size="512" readOnly="true"/>
當(dāng)然也可以不進(jìn)行任何參數(shù)配置,就單獨(dú)聲明下Cache,如下:
<cache/>
我們看一下此時(shí)MappedStatement里Cache的構(gòu)成,可以說是套娃巔峰,把委派模式玩到了極致
我們通過源碼看其實(shí)現(xiàn)
MapperBuilderAssistant.class
// MapperBuilderAssistant.class 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; }
三、二級(jí)緩存的存取
上面我們已經(jīng)看了如何使二級(jí)緩存生效,但真正的查詢和存入還沒有細(xì)看,現(xiàn)在來(lái)看看其存儲(chǔ)的位置,及如何讀取,我們直接看下源碼
1. 緩存源碼分析
// CachingExecutor.class query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) if (cache != null) { // 是否清緩存,即如果Cache配置里含有flushCache=“true” 則進(jìn)行 tcm.clear(); flushCacheIfRequired(ms); if (ms.isUseCache() && resultHandler == null) { ensureNoOutParams(ms, boundSql); @SuppressWarnings("unchecked") // tcm 是一個(gè)成員變量,是通過new TransactionalCacheManager()賦值的,此處為查詢 List<E> list = (List<E>) tcm.getObject(cache, key); if (list == null) { list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); // 通過 tcm 進(jìn)行結(jié)果的存儲(chǔ) tcm.putObject(cache, key, list); // issue #578 and #116 } return list; } }
在進(jìn)行下一步前,我們有必要看一下獲取緩存,為什么要傳兩個(gè)入?yún)?,即cache和key?
- cache:mapper級(jí)別的緩存配置及二級(jí)緩存
- key:方法的全限定名及完整的sql即sql入?yún)?/li>
接下來(lái),我們不難發(fā)現(xiàn)已經(jīng)出現(xiàn)了 存儲(chǔ)、獲取、清除這三種方法,且都圍繞著tcm(TransactionalCacheManager),因此接下來(lái),我們還得關(guān)注一下TransactionalCacheManager。
public class TransactionalCacheManager { private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>(); public Object getObject(Cache cache, CacheKey key) { // 先把緩存設(shè)置作為key,查詢出一個(gè)TransactionalCache return getTransactionalCache(cache).getObject(key); } private TransactionalCache getTransactionalCache(Cache cache) { // 作為第一次查詢,不難發(fā)現(xiàn),所謂的value居然也是依托緩存設(shè)置來(lái)構(gòu)造的 return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new); } // .... } public class TransactionalCache implements Cache { private final Cache delegate; private boolean clearOnCommit; // 臨時(shí)待加緩存,查詢數(shù)據(jù)庫(kù)返回的結(jié)果,首先會(huì)放在這里,等本次事務(wù)提交后,才會(huì)加入到真正的二級(jí)緩存中,即調(diào)用套娃對(duì)象,層層深入,最終存入HashMap // 在事務(wù)提交前,其他會(huì)話甚至本會(huì)話自己都無(wú)法看見該緩存,更無(wú)法使用該緩存 private final Map<Object, Object> entriesToAddOnCommit; // 查二級(jí)緩存沒查到時(shí),會(huì)把key值存在這個(gè)miss的集合中 private final Set<Object> entriesMissedInCache; public TransactionalCache(Cache delegate) { this.delegate = delegate; this.clearOnCommit = false; this.entriesToAddOnCommit = new HashMap<>(); this.entriesMissedInCache = new HashSet<>(); } public Object getObject(Object key) { // issue #116 // 觀察后,可以知道,這里的delegate其實(shí)就是我們說的mapper緩存設(shè)置 // 由此可見,緩存設(shè)置中也能包含緩存的值,并且以完整sql為鍵,sql結(jié)果為值以map存儲(chǔ) Object object = delegate.getObject(key); if (object == null) { entriesMissedInCache.add(key); } // issue #146 if (clearOnCommit) { return null; } else { return object; } } }
由于Cache的套娃十分嚴(yán)重,實(shí)際形成了鏈狀引用,而且受配置的影響很大,所以沒有辦法把每一種配置緩存間的相互調(diào)用闡述詳盡。但是我們?nèi)匀豢梢灾v出其核心思想。忽略掉中間的套娃,最終實(shí)現(xiàn)存儲(chǔ)的緩存類為PerpetualCache.class,其包含了一個(gè)HashMap,鍵就是我們上面提及的key(混合了方法信息,完整sql等),值為sql返回值的字節(jié)數(shù)組
2. 二級(jí)緩存可見性
二級(jí)緩存并不是即時(shí)生效的,我們可以關(guān)注下TransactionalCache 類,這個(gè)類看名字也知道是事務(wù)有關(guān),它有一個(gè)成員變量
private final Map<Object, Object> entriesToAddOnCommit;
這個(gè)變量我們上面源碼分析里其實(shí)說了,就算查數(shù)據(jù)庫(kù),返回了結(jié)果,也不是立即就到我們說的終極位置——PerpetualCache的HashMap里。而是在這個(gè)變量里暫存,等待事務(wù)提交了,再把這里的數(shù)據(jù)存入真正的二級(jí)緩存處
而在此之前,即使是本會(huì)話,也沒法從二級(jí)緩存中撈到東西,也就是說在一個(gè)事務(wù)里,你連續(xù)執(zhí)行兩次同樣的sql,盡管二級(jí)緩存已經(jīng)暫存了數(shù)據(jù),但第二次sql經(jīng)過CachingExecutor時(shí),它并不會(huì)把這個(gè)數(shù)據(jù)給你,那么自然,其他的會(huì)話也是無(wú)法看見這個(gè)數(shù)據(jù)的。
因此,我們說二級(jí)緩存的數(shù)據(jù),只在本事務(wù)提交后才正式可見,在此之前,其他會(huì)話甚至本會(huì)話自己,都無(wú)法使用該二級(jí)緩存
3. 一二級(jí)緩存優(yōu)先級(jí)
看上圖,不難明白,如果開啟了二級(jí)緩存,則先查的是二級(jí)緩存(mapper級(jí)別),這和我們一般的認(rèn)知相悖,因?yàn)榇蠖鄶?shù)緩存層級(jí),都是優(yōu)先查一級(jí)緩存,未命中再去查的二級(jí)緩存。除了上面因?yàn)榭梢娦詥栴}導(dǎo)致的,先查一級(jí)緩存,Mybatis里在一二級(jí)都開啟的情況下,優(yōu)先使用的是二級(jí)緩存的數(shù)據(jù),因此這里需要特別注意。
以上就是 Mybatis的CachingExecutor與二級(jí)緩存的詳細(xì)內(nèi)容,更多關(guān)于 Mybatis的CachingExecutor與二級(jí)緩存的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Mybatis-Spring連接mysql 8.0配置步驟出錯(cuò)的解決方法
這篇文章主要為大家詳細(xì)介紹了Mybatis-Spring連接mysql 8.0配置步驟出錯(cuò)的解決方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-06-06詳解SpringMVC使用MultipartFile實(shí)現(xiàn)文件的上傳
本篇文章主要介紹了SpringMVC使用MultipartFile實(shí)現(xiàn)文件的上傳,本地的文件上傳到資源服務(wù)器上,比較好的辦法就是通過ftp上傳。這里是結(jié)合SpringMVC+ftp的形式上傳的,有興趣的可以了解一下。2016-12-12Java AbstractMethodError原因案例詳解
這篇文章主要介紹了Java AbstractMethodError原因案例詳解,本篇文章通過簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08Mybatis配置之<environments>配置元素詳解
這篇文章主要介紹了Mybatis配置之<environments>配置元素,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-01-01Idea安裝Eslint插件提示:Plugin NativeScript was not installed的問題
這篇文章主要介紹了Idea安裝Eslint插件提示:Plugin NativeScript was not installed的問題,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-10-10Java項(xiàng)目防止SQL注入的幾種方法總結(jié)
SQL注入是比較常見的網(wǎng)絡(luò)攻擊方式之一,在客戶端在向服務(wù)器發(fā)送請(qǐng)求的時(shí)候,sql命令通過表單提交或者url字符串拼接傳遞到后臺(tái)持久層,最終達(dá)到欺騙服務(wù)器執(zhí)行惡意的SQL命令,下面這篇文章主要給大家總結(jié)介紹了關(guān)于Java項(xiàng)目防止SQL注入的幾種方法,需要的朋友可以參考下2023-04-04