一、前言
因為在做項目時候遇到了mybatis緩存的坑,所以全面學習了下mybaits的緩存知識,一來避免后面再次采坑,二來為其他童鞋提供前車之鑒。
二、Mybaits緩存作用
為了提高數據庫查詢性能,緩解數據查詢壓力,后面會具體看到一級是在sqlsession級別緩存了查詢結果和二級緩存則是在namespace級別緩存了查詢結果。
三、Mybaits一級緩存
3.1 問題示例
在做項目時候遇到一個問題,就是數據庫里面有個任務表,單擊頁面上面設備測試時候,會觸發一個事件,這個事件被定時鐘輪詢線程捕獲后,修改任務的狀態,而在單擊設備測試的同時會有一個rpc請求去查看任務狀態,這個rpc的bo層是個循環,循環查詢任務狀態。結果發現定時鐘線程已經修改了任務狀態,但是rpc的bo層循環查找的狀態還是修改前面的,但是明明數據庫里面狀態已經修改了啊。經過斷點發現,rpc的bo層循環查找的結果一直和第一次查找結果一樣,好奇怪,為啥類,第一想法是不是事務隔離性問題啊,畢竟mysql默認配置的隔離水平是Repeated read,但是查看配置我用的mysql是Read Commited,那就郁悶了啊,想知道答案請看3.2
3.2一級緩存原理
Mybatis的一級緩存是SqlSession級別的,我們知道每個mapper接口對應一個SqlSession(這樣說應該不準確,應該是一個線程中一個mapper接口對應一個sqlsession),所以Mybatis的一級緩存在不同mapper之間是隔離,相互不影響的。另外在執行Add,Update,Del時候,會清空當前線程SqlSession的一級緩存避免臟讀。默認情況下mybaits開啟一級緩存。
Mybaits一級緩存結構圖:
screenshot.png
然后我們深入一個SqlSession看看它是怎么玩的?
screenshot.png
劇透下同一個mapper在第一次執行select時候會發現sqlsession緩存沒有記錄,會去數據庫查找,然后把結果保存到緩存,第二次同等條件查詢下,就會從緩存中查找到結果。另外為了避免臟讀,每次執行更新新增刪除時候會清空當前sqlsession緩存。
下面從代碼時序圖看下:
代碼為:
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++;
//從緩存獲取結果
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;
}
由于內部insert,update,delete最終調用的都是update方法,所以看下update代碼:
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
//清空一級緩存
clearLocalCache();
return doUpdate(ms, parameter);
}
由于默認情況下mybatis開啟一級緩存,所以如果你需要每次查詢都從數據庫查詢,可以在mapper.xml里面具體sql語句添加flushCache="true";
<select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Long" flushCache="true">
select
<include refid="Base_Column_List" />
from COMPANY
where ID = #{id,jdbcType=NUMERIC}
and is_deleted = 'n'
</select>
3.1節的問題通過配置這個解決。
四、Mybatis二級緩存
4.1介紹
二級緩存是namespace級別的,這個namespace就是指mapper文件里面那個namepsace,同一個namespace下的搜尋語句共享一個二級緩存。那么二級緩存是怎么樣的構造那,先上個圖:
screenshot.png
4.2 原理
上CachingExecutor的查詢代碼如下:
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
//如果配置了使用二級緩存,則從緩存中取
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, parameterObject, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
//緩存找不到則代理給SimpleExecutor查找,
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
//沒有設置二級緩存,則直接委托給SimpleExecutor查找
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
如果開啟了二級緩存,則先從二級緩存中查找,查找不到則委托為SimpleExecutor查找,而它則會先從一級緩存中查找,查找不到則從數據庫查找。
五、總結
mybaits的二級緩存一般不怎么使用,默認一級緩存是開啟的,如果項目中遇到數據更新后查詢出來的數據卻沒有改變,那么可以從數據隔離性和mybaits緩存方面查找問題所在。