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

