Mybatis的快取
mybatis是一個查詢資料庫的封裝框架,主要是封裝提供靈活的增刪改sql,開發中,service層能夠通過mybatis元件查詢和修改資料庫中表的資料;作為查詢工具,mybatis有使用快取,這裡講一下mybatis的快取相關原始碼。
快取
在計算機裡面,任何資訊都有源頭,快取一般指源頭資訊讀取後,放在記憶體或者其他讀取較快的地方,下次讀取相同資訊不去源頭查詢而是直接從記憶體(或者能快速存取的硬體)讀取。這樣可以減少硬體使用,提高讀取速度。
mybatis也是這樣,查詢資料庫的資料之後,mybatis可以把查詢結果快取到記憶體,下次查詢如果查詢語句相同,並且查詢相關的表的資料沒被修改過,就可以直接返回快取中的結果,而不用去查詢資料庫的語句,有效節省了時間。
關於mybatis中一級和二級快取命名
快取概念較早用於CPU讀取資料,有一級和二級快取,讀取順序是先一級快取,再二級快取。
按照這個概念,通過原始碼瞭解mybatis的Mapper中的快取是一級快取,SqlSession的中快取是二級快取。看到一些介紹mybatis快取的相關文章命名反過來的,稱SqlSession中的快取稱為一級快取,對此有疑惑...
簡單看一下mybatis快取相關原始碼
Mapper中的快取(一級)
mapper中的快取,預設配置是開啟,但需要在對映檔案mapper.xml中新增<cache/>
標籤
<mapper namespace="userMapper">
<cache/><!-- 新增cache標籤表示此mapper使用快取 -->
</mapper>
配置false可以關閉mapper中的快取
mybatis:
configuration:
cache-enabled: false #預設值為true,表示開啟
mapper快取的解析
org.apache.ibatis.builder.xml.XMLMapperBuilder
private void configurationElement(XNode context) {
try {
//...
cacheElement(context.evalNode("cache")); //解析mapper.xml中的cache標籤
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
private void cacheElement(XNode context) {
if (context != null) { // if hava cache tag 如果有cache標籤才執行下面的邏輯
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);//建立mapper快取
}
}
org.apache.ibatis.builder.MapperBuilderAssistant.useNewCache()
:
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);//mapper快取賦值,如果cache標籤為空,不會執行此方法,currentCache為空
currentCache = cache;
return cache;
}
在對映檔案mapper中如果沒有cache標籤,解析時不會執行上面的useNewCache方法,cache為null,就不會使用mapper快取(相當於失效)。
查詢使用mapper快取邏輯
org.apache.ibatis.executor.CachingExecutor
:
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache(); //獲取mapper快取
if (cache != null) {//如果mapper快取物件不為空 嘗試在mapper快取中獲取(沒有cache標籤此物件就是空)
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key); //從mapper快取中獲取資料
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); //如果為空,使用delegate查詢(BaseExecutor)
tcm.putObject(cache, key, list); // 查詢結果儲存到mapper快取
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
上面程式碼中ms物件(MappedStatement )是在系統啟動時建立的物件,cache也是,不與SqlSession繫結,所以SqlSession不同,mapper快取依然可以使用,這區別於SqlSession中的快取。
SqlSession中的快取(二級)
通過檢視原始碼可知,SqlSsession中是有快取的,所以每次(新請求)會話SqlSession不同,快取是空的;相同的SqlSession中的快取才有效。
mybatis預設Sqlsession:org.apache.ibatis.session.defaults.DefaultSqlSession
構造方法中傳入executor(查詢執行物件)
public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
this.configuration = configuration;
this.executor = executor;
this.dirty = false;
this.autoCommit = autoCommit;
}
executor中攜帶二級快取成員變數:
protected BaseExecutor(Configuration configuration, Transaction transaction) {
this.transaction = transaction;
this.deferredLoads = new ConcurrentLinkedQueue<>();
this.localCache = new PerpetualCache("LocalCache"); //預設SqlSession中的快取
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
this.closed = false;
this.configuration = configuration;
this.wrapper = this;
}
查詢使用SqlSession快取邏輯
org.apache.ibatis.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());
List<E> list;
try {
queryStack++;
//localCache SqlSession中的快取
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
//先從SqlSession中的快取中獲取,key是通過sql語句生成
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 如果快取中沒有 才從資料庫查詢
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
return list;
}
//從資料庫讀取資料
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);
} finally {
localCache.removeObject(key);//將SqlSession中的快取清除
}
localCache.putObject(key, list);//返回查詢結果之前,放入SqlSession中的快取 重新整理
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
二級快取和一級快取不用想,資料庫的資料被修改時要清空快取,不然資料有誤;至於怎麼清空,是另一套邏輯,mapper中的cache標籤可以配置一些引數,比如快取定期清空。
一級二級快取先後順序
從概念上來將,先讀取的就是一級快取,後讀取是二級快取,那麼Mapper中的快取是一級快取。
通過newExecutor原始碼可以知道這裡使用了類似裝飾者模式對executor進行了包裝,在建立Executor的時候,邏輯如下
org.apache.ibatis.session.Configuration.newExecutor()
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) {//如果使用快取
executor = new CachingExecutor(executor);//建立CachingExecutor
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
//CachingExecutor構造
public CachingExecutor(Executor delegate) {
this.delegate = delegate; //進行一層包裝 delegate = BaseExecutor
delegate.setExecutorWrapper(this);
}
開啟快取,查詢邏輯從SqlSession.selectList開始,先呼叫CachingExecutor物件
org.apache.ibatis.session.defaults.DefaultSqlSession.selectList()
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
//...
//executor = CachingExecutor
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
}
1 CachingExecutor.query 中嘗試讀取mapper中的快取(一級)
org.apache.ibatis.executor.CachingExecutor.query()
//1 Mapper cache
Cache cache = ms.getCache();
if (cache != null) {
//...
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// delegate.query is BaseExecutor.query
}
return list;
}
}
2 BaseExecutor.query 中嘗試讀取SqlSession中的快取(二級)
org.apache.ibatis.executor.BaseExecutor.query()
//2 SqlSession cache
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
//3 DabaBase
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
3 最後一級二級快取都沒有,就查詢資料庫