MyBatis(三)MyBatis缓存和工作原理
MyBatis緩存
MyBatis提供了一級緩存和二級緩存,并且預留了集成第三方緩存的接口。
從上面MyBatis的包結構可以很容易看出跟緩存相關的類都在cache的package里,其底層是一個Cache的接口,默認的實現類是PerpetualCache,使用一個Map<Object, Object>的哈希Map來緩存數據。此外還有很多的裝飾器類,如下圖所示:從包名就可以猜測出其功能,這里的緩存基本可以分為三類
- 基本緩存:默認的是PerpetualCache,也可以自定義?RedisCache等
- 淘汰算法緩存:FifoCache,LruCache,WeakCache,SoftCache 定義了當緩存內存不足時,淘汰的算法
- 其他裝飾器緩存:BlockingCache等
MyBatis一級緩存
一級緩存也叫本地緩存,MyBatis的一級緩存是在會話(SqlSession)層面進行緩存的。其生命周期也就是Session級別,一旦會話關閉,一級緩存也就不存在了。
MyBatis的一級緩存是默認開啟的,不需要任何的配置,如果想要關閉一級緩存,就把localCacheScope設置成STATEMENT。
查看源碼可以發現在DefaultSqlSession里有一個Executor屬性,在SimpleExecutor/ReuseExecutor/BatchExecutor 的父類BaseExecutor的構造方法里創建了一個PerpetualCache對象用于一級緩存。故而在同一個會話里面(同一個SqlSession),多次執行相同的SQL語句,會直接從PerpetualCache緩存的Map里取到緩存的結果,不會再發送 SQL 到數據庫,簡單的流程如下圖所示
注意:使用一級緩存需要關閉二級緩存,并且將localCacheScope設置成SESSION
<!-- 控制全局緩存(二級緩存) 設置成false則為關閉二級緩存--> <setting name="cacheEnabled" value="false"/> <setting name="localCacheScope" value="SESSION"/>那么MyBatis的一級緩存是以什么為key來判斷某兩次查詢是完全相同的查詢? MyBatis構造了一個CacheKey來表示每一個不同的sql
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {BoundSql boundSql = ms.getBoundSql(parameter);CacheKey key = this.createCacheKey(ms, parameter, rowBounds, boundSql);return this.query(ms, parameter, rowBounds, resultHandler, key, boundSql);}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(rowBounds.getOffset());cacheKey.update(rowBounds.getLimit());cacheKey.update(boundSql.getSql());List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();.... } public class CacheKey implements Cloneable, Serializable {private static final long serialVersionUID = 1146682552656046210L;public static final CacheKey NULL_CACHE_KEY = new NullCacheKey();private static final int DEFAULT_MULTIPLYER = 37;private static final int DEFAULT_HASHCODE = 17;private final int multiplier;private int hashcode;private long checksum;private int count;// 8/21/2017 - Sonarlint flags this as needing to be marked transient. While true if content is not serializable, this is not always true and thus should not be marked transient.private List<Object> updateList;public CacheKey() {//得到初始的hashCode和乘數this.hashcode = DEFAULT_HASHCODE;this.multiplier = DEFAULT_MULTIPLYER;this.count = 0;this.updateList = new ArrayList<>();}//每次添加參數,則將其保存在updateList里,然后計算新加參數的hashCode,更新最新的hashCodepublic void update(Object object) {int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);count++;checksum += baseHashCode;baseHashCode *= count;hashcode = multiplier * hashcode + baseHashCode;updateList.add(object);}//比較兩個CacheKey是否一致@Overridepublic boolean equals(Object object) {if (this == object) {return true;}if (!(object instanceof CacheKey)) {return false;}final CacheKey cacheKey = (CacheKey) object;//hashcode,count,checksum都需要相等if (hashcode != cacheKey.hashcode) {return false;}if (checksum != cacheKey.checksum) {return false;}if (count != cacheKey.count) {return false;}//要求兩個CacheKey的updateList里的每個元素都相等for (int i = 0; i < updateList.size(); i++) {Object thisObject = updateList.get(i);Object thatObject = cacheKey.updateList.get(i);if (!ArrayUtil.equals(thisObject, thatObject)) {return false;}}return true;}從CacheKey的構造可以看出MyBatis認為,如果兩次查詢,以下條件都完全一樣,那么就可以認為它們是完全相同的兩次查詢:
- 傳入的 statementId?
- 查詢時要求的結果集的分頁范圍 (rowBounds.offset和rowBounds.limit,這里是邏輯分頁);
- 本次次查詢要傳遞給數據庫的Sql語句
- sql中的參數值
?
繼續往下就可以看到MyBatis里是如何判斷使用一級緩存的
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 (this.closed) {throw new ExecutorException("Executor was closed.");} else {//對于select語句,flushCahe默認為false,如果配置成true,就會去清空localcache一級緩存if (this.queryStack == 0 && ms.isFlushCacheRequired()) {this.clearLocalCache();}List list;try {++this.queryStack;list = resultHandler == null ? (List)this.localCache.getObject(key) : null;if (list != null) {this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);} else {//如果緩存里沒有,則去查詢數據庫list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);}} finally {--this.queryStack;}if (this.queryStack == 0) {Iterator var8 = this.deferredLoads.iterator();while(var8.hasNext()) {BaseExecutor.DeferredLoad deferredLoad = (BaseExecutor.DeferredLoad)var8.next();deferredLoad.load();}this.deferredLoads.clear();//如果當前mybatis-config.xml配置的localCacheScope是STATEMENT級別,那么也清空緩存,這就是STATEMENT級別的一級緩存無法共享localCache的原因if (this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {this.clearLocalCache();}}return list;}}一級緩存在當前會話執行update(insert,update,delete語句)的時候會調用clearLocalCache()方法清空緩存,但是對于其他會話下的更新不會響應,這就會導致出現數據不一致的問題
public int update(MappedStatement ms, Object parameter) throws SQLException {ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());if (this.closed) {throw new ExecutorException("Executor was closed.");} else {this.clearLocalCache();return this.doUpdate(ms, parameter);}}總結
- Mybatis一級緩存的生命周期和SqlSession一致
- Mybatis的一級緩存是通過PerpetualCache保存的map來做緩存的,沒有更新緩存和緩存過期的機制,也沒有做容量上的限定。
- Mybatis的一級緩存最大范圍是SqlSession內部,有多個SqlSession或者分布式的環境下,同時操作數據庫的話,會引起臟數據
- 可以把一級緩存的默認級別localCacheScope設定為Statement,即不使用一級緩存。
?
MyBatis二級緩存
二級緩存是用來解決一級緩存不能跨會話共享的問題的,范圍是 namespace 級別的,可以被多個SqlSession共享。
MyBatis用了一個裝飾器類來存儲二級緩存數據,就是CachingExecutor。如果啟用了二級緩存,MyBatis在創建Executor對象的時候對Executor進行裝飾。
CachingExecutor對于查詢請求,會判斷二級緩存是否有緩存結果,如果有就直接返回,如果沒有交給真正的查詢器Executor,比如SimpleExecutor來執行查詢,在Executor查詢的時候會再去判斷一級緩存是否存在,最后會把查詢到的結果緩存起來,并且返回給用戶 ?如下圖所示
開啟二級緩存的方式:
步驟1: 在mybatis-config.xml配置<setting name="cacheEnabled" value="true"/> 默認該參數值是true
步驟2:在Mapper.xml中配置<cache/>標簽
如果開啟了二級緩存,那么在創建Executor的時候會使用CachingExecutor裝飾對應的Executor(裝飾器模式)
DefaultSqlSessionFactory:96行 final Executor executor = configuration.newExecutor(tx, execType);public Executor newExecutor(Transaction transaction, ExecutorType executorType) {executorType = executorType == null ? defaultExecutorType : executorType;executorType = executorType == null ? ExecutorType.SIMPLE : executorType;Executor executor;//根據ExecutorType創建不同的執行器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);}//如果開啟了二級緩存,則使用CachingExecutor裝飾Executorif (cacheEnabled) {executor = new CachingExecutor(executor);}//這里是調用插件的方法來增強executor 后面會詳細分析executor = (Executor) interceptorChain.pluginAll(executor);return executor;}所以最后查詢方法會執行CachingExecutor的query方法,代碼如下:
@Overridepublic <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)throws SQLException {//先從MappedStatement中獲取在配置解析時得到的cache//使用了裝飾器模式,具體的執行鏈是SynchronizedCache -> LoggingCache -> SerializedCache -> LruCache -> PerpetualCache。Cache cache = ms.getCache();if (cache != null) {//判斷是否需要刷新緩存flushCacheIfRequired(ms);//如果在mapper.xml里開啟了二級緩存則執行下面的邏輯;否則直接調用原來的executor的query方法if (ms.isUseCache() && resultHandler == null) {//用來處理存儲過程,暫時不考慮ensureNoOutParams(ms, boundSql);@SuppressWarnings("unchecked")List<E> list = (List<E>) tcm.getObject(cache, key);//如果緩存里沒有,則直接執行執行器的query方法,查詢后放入緩存if (list == null) {list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);tcm.putObject(cache, key, list); // issue #578 and #116}return list;}}return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}MyBatis的二級緩存是存放在MappedStatement里的,而MappedStatement是通過Configuration里的
Map<String, MappedStatement> mappedStatements 集合根據statement id獲取的,由于Configuration是全局單例的,所以相同的statement id 對應的MappedStatement也是唯一且相同的,故MyBatis的二級緩存是可以跨SqlSession的
這里MyBatis的二級緩存就是通過TransactionalCacheManager--tcm來管理的(在CachingExecutor里),獲取緩存和添加緩存分別調用了對應的getObject和putObject方法,下面看下tcm的相關源碼
public class TransactionalCacheManager {//緩存查詢結果private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();public void clear(Cache cache) {getTransactionalCache(cache).clear();}public Object getObject(Cache cache, CacheKey key) {return getTransactionalCache(cache).getObject(key);}public void putObject(Cache cache, CacheKey key, Object value) {getTransactionalCache(cache).putObject(key, value);}private TransactionalCache getTransactionalCache(Cache cache) {return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);}}TransactionalCacheManager里持有了一個transactionalCaches的map對象,保存了Cache和用TransactionalCache包裝后的Cache的映射關系。這里的TransactionalCache實現了Cache接口,CachingExecutor會默認使用TransactionalCache包裝初始生成的Cache,TransactionalCacheManager的getObject和putObject實際是調用了TransactionalCache的getObject和putObject方法,源碼如下:
public class TransactionalCache implements Cache {private static final Log log = LogFactory.getLog(TransactionalCache.class);private final Cache delegate;private boolean clearOnCommit;//保存待添加到緩存的key,value,執行commit的時候才會真正添加到緩存private final Map<Object, Object> entriesToAddOnCommit;//保存沒有命中的key 用于計算命中率private final Set<Object> entriesMissedInCache;public TransactionalCache(Cache delegate) {this.delegate = delegate;this.clearOnCommit = false;this.entriesToAddOnCommit = new HashMap<>();this.entriesMissedInCache = new HashSet<>();}@Overridepublic Object getObject(Object key) {//這里直接調用被包裝cache的getObject方法獲取緩存結果// issue #116Object object = delegate.getObject(key);if (object == null) {//如果沒有緩存則添加到miss集合里entriesMissedInCache.add(key);}// issue #146if (clearOnCommit) {return null;} else {return object;}}@Overridepublic void putObject(Object key, Object object) {//將查詢結果的key-value保存到entriesToAddOnCommit集合里entriesToAddOnCommit.put(key, object);}@Overridepublic void clear() {clearOnCommit = true;entriesToAddOnCommit.clear();}public void commit() {if (clearOnCommit) {delegate.clear();}//調用commit的時候執行flushPendingEntries方法,將緩存的key-value真正保存到cache緩存里flushPendingEntries();reset();}private void flushPendingEntries() {for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {delegate.putObject(entry.getKey(), entry.getValue());}for (Object entry : entriesMissedInCache) {if (!entriesToAddOnCommit.containsKey(entry)) {delegate.putObject(entry, null);}}}... }那么這里的commit方法是什么時候調用的呢?猜測是在SqlSession執行commit方法的時候
-------DefaultSqlSession的commit方法 @Overridepublic void commit(boolean force) {try {//對于開啟了二級緩存的Executor,這里的executor是CachingExecutorexecutor.commit(isCommitOrRollbackRequired(force));dirty = false;} catch (Exception e) {throw ExceptionFactory.wrapException("Error committing transaction. Cause: " + e, e);} finally {ErrorContext.instance().reset();}}-------CachingExecutor的commit方法@Overridepublic void commit(boolean required) throws SQLException {delegate.commit(required);//調用了TransactionalCacheManager的commit方法,里面循環所有的TransactionalCache并committcm.commit();}public void commit() {for (TransactionalCache txCache : transactionalCaches.values()) {txCache.commit();}}
上面的源碼分析說明了為什么在使用二級緩存的時候需要commit事務才會將查詢到的數據寫入緩存
如果某些查詢方法對數據的實時性要求很高,無法使用二級緩存,我們可以在單個Statement ID上顯式關閉二級緩存(默認是true)
<select id="selectPerson" resultMap="personResultMap" useCache="false"> //在二級緩存下,如果執行update數據庫更新操作,在更新前會先調用flushCacheIfRequired方法 //然后根據statement 上的 flushCache屬性判斷是否需要刷新緩存 //對于insert,update,delete語句 flushCache默認值都為true,對于select默認值為false public int update(MappedStatement ms, Object parameterObject) throws SQLException {this.flushCacheIfRequired(ms);return this.delegate.update(ms, parameterObject); }private void flushCacheIfRequired(MappedStatement ms) {Cache cache = ms.getCache();if (cache != null && ms.isFlushCacheRequired()) {this.tcm.clear(cache);} }Mybatis的二級緩存一般只推薦在以查詢為主的應用中使用,因為頻繁的更新會導致緩存清空,那么緩存的意義也就不大了。此外,二級緩存比較適合在單表操作的情形下使用,如果在多個不同的namespace下都操作同一張表,因為二級緩存的范圍是namespace,那么一個namespace下的更新無法同步到另外的namespace下,可能會導致出現臟數據
如果想要在多個命名空間中共享相同的緩存配置和實例。可以使用 cache-ref 元素來引用另一個Mapper的緩存。
<cache-ref namespace="com.chenpp.application.data.XXXMapper"/>
除此之外,還可以使用第三方的緩存或者自定義的緩存,比方說redis,ehcache等,使用的時候可以在Mapper.xml里的<cache>標簽指定對應的緩存類型
<cache type="org.mybatis.caches.redis.RedisCache" eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>?如果項目是集群環境,那么推薦使用第三方緩存,比方說redis,可以實現不同集群節點間的緩存共享
?
?
總結
以上是生活随笔為你收集整理的MyBatis(三)MyBatis缓存和工作原理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MyBatis(六)SqlSession
- 下一篇: MongoDB可视化工具--Robo 3