Mybatis緩存
- 介紹
- 一級緩存
-
- 例子
- 結論
- 底層實作
-
- PerpetualCache說明
- CacheKey說明
- LocalCacheScope說明
- 二級緩存
-
- 例子
- 結論
- 底層實作
-
- TransactionalCacheManager說明
- TransactionalCache說明
- 為什麼我們需要送出,二級緩存才有用
介紹
mybatis緩存分一級緩存和二級緩存,其中一級緩存是sqlSession層面的緩存,二級緩存是Mapper層面的緩存。他們都是儲存在Jvm裡的,也就是java對象裡,是以在分布式部署的時候我們就需要實時更新緩存,不然可能會導緻資料一緻性的問題。
一級緩存
例子
上面我們說了一級緩存是sqlSession層面的緩存,現在我們來做個實驗證明一下。
// 啟動相同的兩條查詢,将sql列印出來,看查詢了幾次
public static void main(String[] args){
SqlSession session = sqlSessionFactory.openSession();
// session.selectOne("selectCorp");
UserMapper mapper = session.getMapper(UserMapper.class);
UserDO userDO1 = mapper.getCorpByCorpId("261");
UserDO userDO2 = mapper.getCorpByCorpId("261");
}
從列印結果可以看出隻執行了一次

// 我們分别建立兩個sqlSession來執行,看列印幾次
public static void main(String[] args){
SqlSession session1 = sqlSessionFactory.openSession();
SqlSession session2 = sqlSessionFactory.openSession();
// session.selectOne("selectCorp");
UserMapper mapper1 = session1.getMapper(UserMapper.class);
UserMapper mapper2 = session2.getMapper(UserMapper.class);
UserDO userDO1 = mapper1.getCorpByCorpId("261");
UserDO userDO2 = mapper2.getCorpByCorpId("261");
}
從列印結果看出執行了兩次
結論
當隻建立了一個sqlSession執行兩條相同的sql的時候,隻會查詢一次,當建立兩個sqlSession的時候會分别查詢,說明一級緩存的生命周期和SqlSession一緻
底層實作
我們主要來看BaseExecutor基礎執行器裡的query方法,來說明一級緩存的實作原理
@Override
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());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
//先通過key去取緩存中的集合,如果有就直接傳回,沒用就查詢
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
PerpetualCache說明
我們看到我們的緩存是從localCache擷取的,也就是從PerpetualCache類裡面擷取的,這個是Cache接口的基礎實作類,除了實作了Cache對緩存操作的方法外,還提供了一個map集合,來存儲緩存。Cache除了被這個基礎類實作以外,還被很多其它的類實作(有興趣的可以自己去檢視),通過這些類來裝飾PerpetualCache,使其增加功能
CacheKey說明
CacheKey是我們緩存的key,我們來看一下它是由那些組成的,我們來看BaseExecutor的createCacheKey方法
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// mimic DefaultParameterHandler logic
for (ParameterMapping parameterMapping : parameterMappings) {
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
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 = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
cacheKey.update(value);
}
}
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
從上面的更新參數中可以看出,CacheKey由MappedStatement的id,rowBounds的offset、limit兩個參數,sql語句,查詢參數,Environment的id,共同組成
LocalCacheScope說明
這個類是枚舉類,有兩個參數,分别是SESSION和STATEMENT,從上面的判斷語句我們應該不難看出,當我們配置為STATEMENT的時候,我們執行完sql語句就會立即清除緩存,是以我們在分布式項目當中一定要将這個屬性配置成STATEMENT。
二級緩存
例子
使用二級緩存需要配置兩個地方,一個是全局配置cacheEnabled=true,一個是在mapper裡面配置
public static void main(String[] args){
SqlSession session1 = sqlSessionFactory.openSession(true);
SqlSession session2 = sqlSessionFactory.openSession(true);
// session.selectOne("selectCorp");
UserMapper mapper1 = session1.getMapper(UserMapper.class);
UserMapper mapper2 = session2.getMapper(UserMapper.class);
UserDO userDO1 = mapper1.getCorpByCorpId("261");
//session1.commit();
UserDO userDO2 = mapper2.getCorpByCorpId("261");
}
我們先把commit這個操作關了看一下列印結果:執行了兩次
當我們commit之後,看看列印結果:執行了一次

結論
當我們使用二級緩存的時候,即使在不同的sqlSession裡,隻要執行語句相同,我們也能使用緩存,前提是需要送出上一次查詢
底層實作
我們主要來看一下CachingExecutor類下面的query方法,CachingExecutor是一個裝飾類,主要用來裝飾baseExecuor執行器,當我們配置了全局屬性cacheEnabled=true,就會使用CachingExecutor類。
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
//這個cache就是我們在mapper裡面配置的cache,如果不配置就不能用兩級緩存
Cache cache = ms.getCache();
if (cache != null) {
//是否重新整理緩存
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
從上面可以看出來當我們在裡面配置的cache,我們程式就會走二級緩存。
TransactionalCacheManager說明
這個裡面維持着一個map,是以cache為key的,所有說二級緩存是針對mapper層面的緩存
TransactionalCache說明
這個就是二級緩存最終存儲資料的地方,想到與一級緩存,是以我們可以知道二級緩存就是同一個mapper裡的一級緩存的集合
為什麼我們需要送出,二級緩存才有用
首先我們将我們的緩存存在了TransactionalCache的entriesToAddOnCommit裡
而我們取是通過key去SynchronizedCache裡取的
是以當我們沒用送出的時候,資料還沒用到SynchronizedCache裡,隻是在entriesToAddOnCommit裡存着,我們再來看看sqlSession commit做了什麼,如圖:最終是将我們儲存在entriesToAddOnCommit裡的資料周遊到PerpetualCache中的map裡