您好,登錄后才能下訂單哦!
這篇文章將為大家詳細講解有關如何理解Mybatis源碼中的Cache,文章內容質量較高,因此小編分享給大家做個參考,希望大家閱讀完這篇文章后對相關知識有一定的了解。
通過復雜業務計算得來的數據,在計算過程中可能耗費大量的時間,需要將數據緩存
讀多寫少的數據
緩存的容量
緩存的有效時間
訪問的緩存不存在,直接去訪問數據庫。通常查找的key沒有對應的緩存,可以設計為返回空值,不去查找數據庫。
大量的緩存穿透會導致有大量請求,訪問都會落到數據庫上,造成緩存雪崩。所以如果訪問的key在緩存中找不到,不要直接去查詢數據庫,也就是要避免緩存穿透,可以設置緩存為永久緩存,然后通過后臺定時更新緩存。也可以為緩存更新添加鎖保護,確保當前只有一個線程更新數據。
使用范圍:SESSION,STATEMENT。默認為SESSION,如果不想使用一級緩存,可以修改為STATEMENT,每次調用后都會清掉緩存。
//STATEMENT或者SESSION,默認為SESSION <setting name="localCacheScope" value="STATEMENT"/>
一級緩存執行的流程:
//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 { ... //先根據cachekey從localCache去查 list = resultHandler == null ? (List<E>) localCache.getObject(key) : null; if (list != null) { //若查到localCache緩存,處理localOutputParameterCache handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); } else { //從數據庫查 list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); } } finally { //清空堆棧 queryStack--; } ... if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { // issue #482 //如果是STATEMENT,清本地緩存 clearLocalCache(); } return list; }
什么情況下有效?在,一個sqlSession打開和關閉的范圍內,所以,如果被Spring托管并且沒有開啟事務,那么一級緩存是失效的。
注意: 打開兩個sqlsession: sqlsession1,sqlsession2,sqlsession1.select從一級緩存中取數據,sqlsession2更新此條數據,sqlsession1.select再次獲取數據,還是緩存中的。
SessionFactory層面給各個SqlSession 對象共享
怎么配置:
<setting name="cacheEnabled" value="true" /> (或@CacheNamespace) <cache/>,<cache-ref/>或@CacheNamespace //每個select設置 <select id="queryDynamicFlightVOs" resultType="com.ytkj.aoms.aiis.flightschedule.vo.DynamicFlightVO" useCache="false">
如果配置了二級緩存,那么在獲取Executor的時候會返回CachingExecutor
//Configuration.java //產生執行器 public Executor newExecutor(Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? defaultExecutorType : executorType; //這句再做一下保護,,防止粗心大意的人將defaultExecutorType設成null? executorType = executorType == null ? ExecutorType.SIMPLE : executorType; Executor executor; //然后就是簡單的3個分支,產生3種執行器BatchExecutor/ReuseExecutor/SimpleExecutor 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(默認就是有緩存),裝飾者模式,所以默認都是返回CachingExecutor if (cacheEnabled) { executor = new CachingExecutor(executor); } //此處調用插件,通過插件可以改變Executor行為 executor = (Executor) interceptorChain.pluginAll(executor); return executor; }
開啟了二級緩存后,會先使用CachingExecutor查詢,查詢不到在查一級緩存。
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { //namespace配置作用 Cache cache = ms.getCache(); if (cache != null) { //是否執行刷新緩存操作 this.flushCacheIfRequired(ms); //useCache標簽作用 if (ms.isUseCache() && resultHandler == null) { this.ensureNoOutParams(ms, parameterObject, boundSql); List<E> list = (List)this.tcm.getObject(cache, key); if (list == null) { list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); this.tcm.putObject(cache, key, list); } return list; } } return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }
由于二級緩存中的數據是基于namespace的,即不同namespace中的數據互不干擾。在多個namespace中若均存在對同一個表的操作,那么這多個namespace中的數據可能就會出現不一致現象
在分布式環境下,由于默認的MyBatis Cache實現都是基于本地的,分布式環境下必然會出現讀取到臟數據,需要使用集中式緩存將MyBatis的Cache接口實現,有一定的開發成本,直接使用Redis、Memcached等分布式緩存可能成本更低,安全性也更高
通過裝飾器模式,我們可以向一個現有的對象添加新的功能,同時不改變其結構。
Mybatis中不同類型的緩存,正是使用了此類設計。
PerpetualCache實現了Cache接口,完成了基本的Cache功能,其他的裝飾類對其進行功能擴展。以LruCache為例:
public class LruCache implements Cache { //持有實現類 private final Cache delegate; //額外用了一個map才做lru,但是委托的Cache里面其實也是一個map,這樣等于用2倍的內存實現lru功能 private Map<Object, Object> keyMap; private Object eldestKey; public LruCache(Cache delegate) { this.delegate = delegate; setSize(1024); } ..... }
LruCache使用了一個LinkedHashMap作為keyMap,最多存1024個key的值,超過之后新加對象到緩存時,會將不經常使用的key從keyMap中移除,并且刪除掉緩存中對應的key。利用了LinkedHashMap的特性:
每次訪問或者插入一個元素都會把元素放到鏈表末尾
//LinkedHashMap的get方法 public V get(Object key) { Node<K,V> e; if ((e = getNode(hash(key), key)) == null) return null; if (accessOrder) afterNodeAccess(e); return e.value; }
調用put,putAll方法的時候會調用removeEldestEntry方法,所以LruCache在new LinkedHashMap的時候重寫了removeEldestEntry方法
public void setSize(final int size) { keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) { private static final long serialVersionUID = 4267176411845948333L; //核心就是覆蓋 LinkedHashMap.removeEldestEntry方法, //返回true或false告訴 LinkedHashMap要不要刪除此最老鍵值 //LinkedHashMap內部其實就是每次訪問或者插入一個元素都會把元素放到鏈表末尾, //這樣不經常訪問的鍵值肯定就在鏈表開頭啦 @Override protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) { boolean tooBig = size() > size; if (tooBig) { //根據eldestKey去緩存中刪除 eldestKey = eldest.getKey(); } return tooBig; } }; } private void cycleKeyList(Object key) { keyMap.put(key, key); //keyMap是linkedhashmap,最老的記錄已經被移除了,然后這里我們還需要移除被委托的那個cache的記錄 if (eldestKey != null) { delegate.removeObject(eldestKey); eldestKey = null; } }
BlockingCache:阻塞版本的緩存裝飾器,它保證只有一個線程到數據庫中查找指定key對應的數據, 加入線程A 在BlockingCache 中未查找到keyA對應的緩存項時,線程A會獲取keyA對應的鎖,這樣后續線程在查找keyA是會發生阻塞。
FifoCache:先進先出緩存,FifoCache在putObject的時候會將Key放到一個List中,list長度為1024,如果超過,則刪除第一個緩存,最后一位添加當前緩存
LoggingCache:日志緩存,它持有日志接口,根據日志的設置可打印sql和命中率等。
LruCache:最近最少使用算法。核心思想是當緩存滿時,會優先淘汰那些近期最少使用的緩存。
ScheduledCache:定時調度緩存,每次訪問和添加緩存的時候會判斷當前時間和上次清理的時間是否超過閾值,超過則會清理緩存。
SerializedCache:序列化緩存。緩存的值會被序列化。取值的時候會反序列化。要求緩存的對象實現Serializable接口。
SoftCache:軟引用緩存。對于軟引用關聯著的對象,只有在內存不足的時候JVM才會回收該對象,使用SoftReference可以避免OOM異常
WeakCache:弱引用緩存,類似SoftCache。與軟引用區別在于當JVM進行垃圾回收時,無論內存是否充足,都會回收被弱引用關聯的對象。
TransactionalCache:事務緩存
關于如何理解Mybatis源碼中的Cache就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。