MyBatis將數(shù)據(jù)緩存設(shè)計成兩級結(jié)構(gòu),分為一級緩存酝碳、二級緩存:
一級緩存是Session會話級別的緩存:表示一次數(shù)據(jù)庫會話的SqlSession對象之中伴网,又被稱之為本地緩存逃沿。一級緩存是MyBatis內(nèi)部實現(xiàn)的一個特性透揣,用戶不能配置济炎,默認(rèn)情況下自動支持的緩存,用戶沒有定制它的權(quán)利(不過這也不是絕對的辐真,可以通過開發(fā)插件對它進(jìn)行修改)须尚;
二級緩存是Application應(yīng)用級別的緩存:它的是生命周期很長,跟Application的聲明周期一樣侍咱,也就是說它的作用范圍是整個Application應(yīng)用耐床。
由于MyBatis
使用SqlSession
對象表示一次數(shù)據(jù)庫的會話,為了減少資源的浪費楔脯,MyBatis
會在表示會話的SqlSession
對象中建立一個簡單的緩存撩轰,將每次查詢到的結(jié)果結(jié)果緩存起來,當(dāng)下次查詢的時候,如果判斷先前有個完全一樣的查詢堪嫂,會直接從緩存中直接將結(jié)果取出偎箫,返回給用戶,不需要再進(jìn)行一次數(shù)據(jù)庫查詢了溉苛。
實際上,
SqlSession
只是一個MyBatis對外的接口镜廉,SqlSession將它的工作交給了Executor執(zhí)行器這個角色來完成弄诲,負(fù)責(zé)完成對數(shù)據(jù)庫的各種操作愚战。當(dāng)創(chuàng)建了一個SqlSession對象時,MyBatis會為這個SqlSession對象創(chuàng)建一個新的Executor執(zhí)行器齐遵,而緩存信息就被維護在這個Executor執(zhí)行器中寂玲,MyBatis將緩存和對緩存相關(guān)的操作封裝成了Cache接口中,Executor接口的實現(xiàn)類BaseExecutor中擁有一個Cache接口的實現(xiàn)類PerpetualCache梗摇,則對于BaseExecutor對象而言拓哟,它將使用PerpetualCache對象維護緩存
public class PerpetualCache implements Cache {
private String id;
private Map<Object, Object> cache = new HashMap();
public PerpetualCache(String id) {
this.id = id;
}
public String getId() {
return this.id;
}
public int getSize() {
return this.cache.size();
}
public void putObject(Object key, Object value) {
this.cache.put(key, value);
}
public Object getObject(Object key) {
return this.cache.get(key);
}
public Object removeObject(Object key) {
return this.cache.remove(key);
}
public void clear() {
this.cache.clear();
}
public ReadWriteLock getReadWriteLock() {
return null;
}
public boolean equals(Object o) {
if(this.getId() == null) {
throw new CacheException("Cache instances require an ID.");
} else if(this == o) {
return true;
} else if(!(o instanceof Cache)) {
return false;
} else {
Cache otherCache = (Cache)o;
return this.getId().equals(otherCache.getId());
}
}
public int hashCode() {
if(this.getId() == null) {
throw new CacheException("Cache instances require an ID.");
} else {
return this.getId().hashCode();
}
}
}
一級緩存的生命周期
MyBatis
在開啟一個數(shù)據(jù)庫會話時,會 創(chuàng)建一個新的SqlSession
對象伶授,SqlSession
對象中會有一個新的Executor
對象断序,Executor
對象中持有一個新的PerpetualCache
對象;當(dāng)會話結(jié)束時糜烹,SqlSession
對象及其內(nèi)部的Executor
對象還有PerpetualCache
對象也一并釋放掉违诗。- 如果
SqlSession
調(diào)用了close()
方法,會釋放掉一級緩存PerpetualCache
對象疮蹦,一級緩存將不可用诸迟;- 如果
SqlSession
調(diào)用了clearCache()
,會清空PerpetualCache
對象中的數(shù)據(jù)愕乎,但是該對象仍可使用阵苇;
4.SqlSession
中執(zhí)行了任何一個update
操作(update()、delete()感论、insert())
绅项,都會清空PerpetualCache
對象的數(shù)據(jù),但是該對象可以繼續(xù)使用比肄;
一級緩存的實現(xiàn)
- 對于某個查詢快耿,根據(jù)statementId,params,rowBounds來構(gòu)建一個key值,根據(jù)這個key值去緩存Cache中取出對應(yīng)的key值存儲的緩存結(jié)果薪前;
- 判斷從Cache中根據(jù)特定的key值取的數(shù)據(jù)數(shù)據(jù)是否為空润努,即是否命中;
- 如果命中示括,則直接將緩存結(jié)果返回铺浇;
- 如果沒命中:
1. 去數(shù)據(jù)庫中查詢數(shù)據(jù),得到查詢結(jié)果垛膝;
2. 將key和查詢到的結(jié)果分別作為key,value對存儲到Cache中鳍侣;
3. 將查詢結(jié)果返回丁稀; - 結(jié)束。
Cache接口的設(shè)計以及CacheKey的定義
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
//構(gòu)建緩存需要的key值
CacheKey key = this.createCacheKey(ms, parameter, rowBounds, boundSql);
return this.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
//構(gòu)建緩存需要的key值
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if(this.closed) {
throw new ExecutorException("Executor was closed.");
} else {
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId());
cacheKey.update(Integer.valueOf(rowBounds.getOffset()));
cacheKey.update(Integer.valueOf(rowBounds.getLimit()));
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
for(int i = 0; i < parameterMappings.size(); ++i) {
ParameterMapping parameterMapping = (ParameterMapping)parameterMappings.get(i);
if(parameterMapping.getMode() != ParameterMode.OUT) {
String propertyName = parameterMapping.getProperty();
Object value;
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 = this.configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
cacheKey.update(value);
}
}
return cacheKey;
}
}
調(diào)用如下方法時倚聚,debug源碼的截圖
//mapper接口的方法 schoolCustomerDao.selectBySome(1l, "2017-09-17","120706049");
CacheKey
構(gòu)建好了之后线衫,就可以存儲查詢后的結(jié)果了,源碼如下所示:
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
this.localCache.putObject(key, ExecutionPlaceholder.EXECUTION_PLACEHOLDER);
List list;
try {
list = this.doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
this.localCache.removeObject(key);
}
//緩存查詢的結(jié)果
this.localCache.putObject(key, list);
if(ms.getStatementType() == StatementType.CALLABLE) {
this.localOutputParameterCache.putObject(key, parameter);
}
return list;
}
得出CacheKey由以下條件可以得到:statementId + rowBounds + 傳遞給JDBC的SQL + 傳遞給JDBC的參數(shù)值
一級緩存的性能分析
我將從兩個 一級緩存的特性來討論SqlSession的一級緩存性能問題:
- MyBatis對會話(Session)級別的一級緩存設(shè)計的比較簡單惑折,就簡單地使用了HashMap來維護授账,并沒有對HashMap的容量和大小進(jìn)行限制。
讀者有可能就覺得不妥了:如果我一直使用某一個SqlSession對象查詢數(shù)據(jù)惨驶,這樣會不會導(dǎo)致HashMap太大白热,而導(dǎo)致 java.lang.OutOfMemoryError錯誤啊粗卜? 讀者這么考慮也不無道理屋确,不過MyBatis的確是這樣設(shè)計的。
MyBatis這樣設(shè)計也有它自己的理由:
- 一般而言SqlSession的生存時間很短续扔。一般情況下使用一個SqlSession對象執(zhí)行的操作不會太多攻臀,執(zhí)行完就會消亡;
- 對于某一個SqlSession對象而言纱昧,只要執(zhí)行update操作(update刨啸、insert、delete)砌些,都會將這個SqlSession對象中對應(yīng)的一級緩存清空掉呜投,所以一般情況下不會出現(xiàn)緩存過大,影響JVM內(nèi)存空間的問題存璃;
- 可以手動地釋放掉SqlSession對象中的緩存仑荐。
- 一級緩存是一個粗粒度的緩存,沒有更新緩存和緩存過期的概念
MyBatis的一級緩存就是使用了簡單的HashMap纵东,MyBatis只負(fù)責(zé)將查詢數(shù)據(jù)庫的結(jié)果存儲到緩存中去粘招, 不會去判斷緩存存放的時間是否過長、是否過期偎球,因此也就沒有對緩存的結(jié)果進(jìn)行更新這一說了洒扎。
根據(jù)一級緩存的特性,在使用的過程中衰絮,我認(rèn)為應(yīng)該注意:
- 對于數(shù)據(jù)變化頻率很大袍冷,并且需要高時效準(zhǔn)確性的數(shù)據(jù)要求,我們使用SqlSession查詢的時候猫牡,要控制好SqlSession的生存時間胡诗,SqlSession的生存時間越長,它其中緩存的數(shù)據(jù)有可能就越舊,從而造成和真實數(shù)據(jù)庫的誤差煌恢;同時對于這種情況骇陈,用戶也可以手動地適時清空SqlSession中的緩存;
- 對于只執(zhí)行瑰抵、并且頻繁執(zhí)行大范圍的select操作的SqlSession對象你雌,SqlSession對象的生存時間不應(yīng)過長。