在《Mybatis一級緩存原了解析》一文中我們知道了Mybatis的一級緩存是在BaseExecutor執行器中(如果隻使用一級緩存,那麼隻需要建立SimpleExecutor即可,因為SimpleExecutor繼承BaseExecutor),也提到了二級緩存在CachingExecutor中。但是在Mybatis建立執行器的過程中(newExecutor)為何要通過裝飾器設計模式建立執行器CachingExecutor呢?
隻想看如何使用,則可以直接看總結!
我們先回顧一下這段代碼:
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
Executor executor;
//然後就是簡單的3個分支,産生3種執行器BatchExecutor/ReuseExecutor/SimpleExecutor
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
//我們知道上面傳的是SIMPLE,是以會建立SimpleExecutor,SimpleExecutor繼承BaseExecutor
executor = new SimpleExecutor(this, transaction);
}
//cacheEnabled預設就是true,是以必然傳回的就是CachingExecutor
//如果要求緩存,生成另一種CachingExecutor(預設就是有緩存),裝飾者模式,是以預設都是傳回CachingExecutor
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
//此處調用插件,通過插件可以改變Executor行為,pageHepler就是在這裡增加了插件
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
通過上面的我們可以看到,如果不使用二級緩存,可以将cacheEnabled這個boolean類型字段置為false(預設是true):
<setting name="cacheEnabled" value="false"/>
那麼既然預設為true,那麼它所有的查詢都會走二級緩存嗎?
帶着這個問題,我們來研究一下Mybatis的二級緩存:
我們先回到在DefaultSqlSession的查詢方法中
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
//根據statement id找到對應的MappedStatement
MappedStatement ms = configuration.getMappedStatement(statement);
//轉而用執行器來查詢結果,注意這裡傳入的ResultHandler是null
//這裡我們可以看到使用的是executor,這裡的executor我們需要重點看一下,這裡是CachingExecutor!!!
//我們來看這行代碼
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();
}
}
然後我們才看CachingExecutor的query方法:
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
//這裡會先判斷目前的MappedStatement有沒有cache,如果沒有,則會直接查詢一級緩存
//這裡的緩存擷取看下面的代碼即可
Cache cache = ms.getCache();
//預設情況下是沒有開啟緩存的(二級緩存).要開啟二級緩存,你需要在你的 SQL 映射檔案中添加一行: <cache/>
//簡單的說,就是先查CacheKey,查不到再委托給實際的執行器去查
if (cache != null) {
//這裡對應的Statement中的flushCache="true",即在執行某個sql前是否重新整理緩存
flushCacheIfRequired(ms);
//UseCache對應select标簽中的useCache="true",查詢結果是否進行緩存
if (ms.isUseCache() && resultHandler == null) {
//開始從二級緩存中查詢資料
List<E> list = (List<E>) tcm.getObject(cache, key);
//沒有資料則查詢SimpleExecutor
if (list == null) {
//我們知道delegate=SimpleExecutor,而SimpleExecutor繼承了BaseExecutor
//這裡實際就是調用的BaseExecutor的query方法!!!
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
//存入二級緩存
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
//如果上面的二級緩存沒有查到資料,則會執行SimpleExecutor的query方法,由于一級緩存預設開啟
//會先去查一級緩存,若沒有資料,則會查詢資料庫
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
通過上面的代碼我們知道了Cache cache = ms.getCache();,如果目前的cache為null,則直接查詢SimpleExecutor,這裡說明如果我們沒有配置Cache,則不會走二級緩存。
那麼Cache是什麼呢?
<cache type="org.apache.test.BusinessRequestMonitorLogCache"/>
調用鍊:parse->parseConfiguration->mapperElement->parse->configurationElement->cacheElement()
篇幅問題,我們直接看:XMLMapperBuilder#cacheElement()
//這裡是解析上面的xml中的cache标簽
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);
//讀入額外的配置資訊,易于第三方的緩存擴充,例:
// <cache type="com.domain.something.MyCustomCache">
// <property name="cacheFile" value="/tmp/my-custom-cache.tmp"/>
// </cache>
Properties props = context.getChildrenAsProperties();
//調用builderAssistant.useNewCache
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
//這裡面又判斷了一下是否為null就用預設值,有點和XMLMapperBuilder.cacheElement邏輯重複了
//這裡如果類型為空,則使用預設的二級緩存PerpetualCache
typeClass = valueOrDefault(typeClass, PerpetualCache.class);
evictionClass = valueOrDefault(evictionClass, LruCache.class);
//調用CacheBuilder建構cache,id=currentNamespace
Cache cache = new CacheBuilder(currentNamespace)
.implementation(typeClass)
.addDecorator(evictionClass)
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
//加入緩存
configuration.addCache(cache);
//目前的緩存
currentCache = cache;
return cache;
}
通過上面的代碼把讀取到的緩存對象指派給currentCache,這個currentCache是Mapper級别的,而Mybatis在解析每一個Statement标簽時通過MapperBuilderAssistant#addMappedStatement方法将currentCache指派給了每一個Statement标簽
setStatementCache(isSelect, flushCache, useCache, currentCache, statementBuilder);
private void setStatementCache(
//是否是查詢語句
boolean isSelect,
//是否重新整理緩存
boolean flushCache,
//是否使用緩存
boolean useCache,
//緩存資訊
Cache cache,
MappedStatement.Builder statementBuilder) {
flushCache = valueOrDefault(flushCache, !isSelect);
useCache = valueOrDefault(useCache, isSelect);
statementBuilder.flushCacheRequired(flushCache);
statementBuilder.useCache(useCache);
statementBuilder.cache(cache);
}
通過上面的代碼,Mybatis将我們設定的<cache type="org.apache.test.BusinessRequestMonitorLogCache"/>指派給每一個Statement标簽。
我們來簡單總結一下今天的内容:
Mybatis預設使用的執行器就是二級緩存執行器,不想使用可以關閉:
<setting name="cacheEnabled" value="false"/>
即便我們不關閉,Mybatis執行sql依然使用的是二級緩存執行器,但每次查詢并不會使用二級緩存,想要使用二級緩存,我們需要先在Mapper.xml中配置:
<cache type="org.apache.test.BusinessRequestMonitorLogCache"/>
如果不設定type标簽,則使用預設的二級緩存:PerpetualCache,不知道大家對這個名字熟悉不,這裡使用的是跟一級緩存一樣的本地HashMap
如果設定了type标簽,則會使用我們自定義的緩存,如果我們想內建第三方緩存配件(如redis),則可以像我一樣,建立我們自定義緩存類,實作Cache接口,重寫對應的方法即可!
即便配置了Mapper級别的緩存,每一個查詢Statement依然不會走二級緩存,需要在對應的select标簽中加上useCache="true"。
如果想執行某個sql前清除緩存,則可以在對應的标簽中加上flushCache="true"
通過以上的配置,在對應的select查詢時會調用二級緩存!
需要注意的是:二級緩存是事務性的(關于二級緩存事務,由于篇幅,這裡隻做簡單了解)。這意味着,當 SqlSession 完成并送出時,或是完成并復原,但沒有執行 flushCache=true 的 insert/delete/update 語句時,緩存會獲得更新。
如果設定了事務手動送出,則事務不送出,二級緩存不存在
為什麼事務不送出,二級緩存不生效?因為二級緩存使用TransactionalCacheManager來管理,當進行putObject即存入二級緩存時,隻是添加到了entriesToAddOnCommit (也是個map,可以了解為臨時存放)裡面,隻有它的commit()方法被調用的時候才會調用flushPendingEntries()真正寫入到我們的二級緩存中。