看Mybatis如何花樣設計 Cache
為什麼說花樣設計 Cache , 是因為 Mybatis只是對 Map數據結構的封裝, 但是卻實現了很多挺好用的能力。如果單單從設計模式上的角度來,其實就是典型的裝飾器模式, 裝飾器模式其實並不難,所以我們不講設計模式, 本篇文章我們來看看 Mybatils 緩存設計巧妙的點。
通過簡單的代碼review來分析下這十個緩存類設計的巧妙點。
一、模式分析
從目錄就很清晰看出,核心就是 impl 包下面只有一個,其他都是裝飾器模式,在 decorators 包下
1. Cache
接口設計沒有什麼好講的,提供獲取和添加方法,跟Map接口一樣。 本篇我們要一起Review的類都會實現該接口的。
(這句話簡直就是廢話,大佬勿噴,就是簡單提醒。意思就是其實代碼不難)
<code>public interface Cache { String getId(); void putObject(Object key, Object value); Object getObject(Object key); Object removeObject(Object key); void clear(); int getSize(); ReadWriteLock getReadWriteLock();}/<code>
2. PerpetualCache
這個類就是 Mybatis 緩存最底層的設計, 看一下就知道其實是對 Map 的封裝。 其實我們只要知道他是簡單的 HashMap 的封裝就可以了
<code>public class PerpetualCache implements Cache { // 唯一標識 private final String id; // 就是一個HashMap結構 private Map<object> cache = new HashMap<object>(); public PerpetualCache(String id) { this.id = id; } @Override public String getId() { return id; } @Override public int getSize() { return cache.size(); } @Override public void putObject(Object key, Object value) { cache.put(key, value); } @Override public Object getObject(Object key) { return cache.get(key); } @Override public Object removeObject(Object key) { return cache.remove(key); } @Override public void clear() { cache.clear(); } // 基本沒啥用,外層誰要用,誰重寫 @Override public ReadWriteLock getReadWriteLock() { return null; } @Override public boolean equals(Object o) { if (getId() == null) { throw new CacheException("Cache instances require an ID."); } if (this == o) { return true; } if (!(o instanceof Cache)) { return false; } Cache otherCache = (Cache) o; return getId().equals(otherCache.getId()); } @Override public int hashCode() { if (getId() == null) { throw new CacheException("Cache instances require an ID."); } return getId().hashCode(); }}/<object>/<object>/<code>
3. 小總結
其實上面就是 Mybatis 關於 Cache 的核心實現,其實看到這裡還沒有很多知識點. 那麼我們從中能學到什麼呢? 如果真要找一條學習的點,那麼就是:
設計要面向接口設計,而不是具體實現。 這樣當我們要重寫 Cache ,比如說我們不想底層用 HashMap 來實現了,其實我們只要實現一下 Cache 接口,然後替換掉 PerpetualCache就可以了。對於使用者其實並不感知。
二、開始重頭戲
從這裡我們主要一起看下,代碼設計的巧妙之處,一個一個研究下,以下這10個類。看 Mybatis 是如何巧妙設計的。
1. BlockingCache
BlockingCache是一個簡單和低效的 Cache的裝飾器,我們主要看幾個重要方法。
<code>public class BlockingCache implements Cache { private long timeout; //實現Cache接口的緩存對象 private final Cache delegate; //對每個key生成一個鎖對象 private final ConcurrentHashMap<object> locks; public BlockingCache(Cache delegate) { this.delegate = delegate; this.locks = new ConcurrentHashMap<object>(); } @Override public String getId() { return delegate.getId(); } @Override public int getSize() { return delegate.getSize(); } @Override public void putObject(Object key, Object value) { try { delegate.putObject(key, value); } finally { //釋放鎖。 為什麼不加鎖? 所以get和put是組合使用的,當get加鎖,如果沒有就查詢數據庫然後put釋放鎖,然後其他線程就可以直接用緩存數據了。 releaseLock(key); } } @Override public Object getObject(Object key) { //1. 當要獲取一個key,首先對key進行加鎖操作,如果沒有鎖就加一個鎖,有鎖就直接鎖 acquireLock(key); Object value = delegate.getObject(key); if (value != null) { //2. 如果緩存命中,就直接解鎖 releaseLock(key); } //3. 當value=null, 就是說沒有命中緩存,那麼這個key就會被鎖住,其他線程進來都要等待 return value; } @Override public Object removeObject(Object key) { // 移除key的時候,順便清楚緩存key的鎖對象 releaseLock(key); return null; } @Override public void clear() { delegate.clear(); } @Override public ReadWriteLock getReadWriteLock() { return null; } private ReentrantLock getLockForKey(Object key) { ReentrantLock lock = new ReentrantLock(); ReentrantLock previous = locks.putIfAbsent(key, lock); //如果key對應的鎖存在就返回,沒有就創建一個新的 return previous == null ? lock : previous; } private void acquireLock(Object key) { Lock lock = getLockForKey(key); //1. 如果設置超時時間,就可以等待timeout時間(如果超時了報錯) if (timeout > 0) { try { boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS); if (!acquired) { throw new CacheException("Couldn't get a lock in " + timeout + " for the key " + key + " at the cache " + delegate.getId()); } } catch (InterruptedException e) { throw new CacheException("Got interrupted while trying to acquire lock for key " + key, e); } } else { //2. 如果沒有設置,直接就加鎖(如果這個鎖已經被人用了,那麼就一直阻塞這裡。等待上一個釋放鎖) lock.lock(); } } private void releaseLock(Object key) { ReentrantLock lock = locks.get(key); if (lock.isHeldByCurrentThread()) { lock.unlock(); } } public long getTimeout() { return timeout; } public void setTimeout(long timeout) { this.timeout = timeout; } }/<object>/<object>/<code>
建議看代碼註釋
方法解釋acquireLock加鎖操作getObject進來加鎖,如果緩存存在就釋放鎖,不存在就不釋放鎖。putObject添加元素並釋放鎖removeObject移除key的時候,順便清楚緩存key的鎖對象getLockForKey如果key對應的鎖存在就返回,沒有就創建一個新的
思考
- 這個因為每次key請求都會加lock真的會很慢嗎? 我們舉兩種場景。
注意這個加lock並不是對get方法加lock,而是對每個要get的key來加lock。
場景一: 試想一種場景,當有10個線程同時從數據庫查詢一個key為123的數據時候,當第一個線程來首先從cache中讀取時候,這個時候其他九個線程是會阻塞的,因為這個key已經被加lock了。當第一個線程get這個key完成時候,其他線程才能繼續走。這種場景來說是不好的,
場景二: 但是當第一個線程來發現cache裡面沒有數據這個時候其他線程會阻塞,而第一個線程會從db中查詢,然後在put到cache裡面。這樣其他9個線程就不需要在去查詢db了,就減少了9次db查詢。
2. FifoCache
FIFO( First Input First Output),簡單說就是指先進先出
如何實現先進先出呢? 其實非常簡單,當put時候,先判斷是否需要執行淘汰策略,如果要執行淘汰,就 移除先進來的。 直接通過 Deque API 來實現先進先出。
<code> private final Cache delegate; private final Deque<object> keyList; private int size; public FifoCache(Cache delegate) { this.delegate = delegate; this.keyList = new LinkedList<object>(); this.size = 1024; }@Override public void putObject(Object key, Object value) { //1. put時候就判斷是否需要淘汰 cycleKeyList(key); delegate.putObject(key, value); } private void cycleKeyList(Object key) { keyList.addLast(key); //1. size默認如果大於1024就開始淘汰 if (keyList.size() > size) { //2. 利用Deque隊列移除第一個。 Object oldestKey = keyList.removeFirst(); delegate.removeObject(oldestKey); } }/<object>/<object>/<code>
3. LoggingCache
從名字上看就是跟日誌有關, LoggingCache 會在 debug級別下把緩存命中率給統計出來,然後通過日誌系統打印出來。
<code>public Object getObject(Object key) { requests++; final Object value = delegate.getObject(key); if (value != null) { hits++; } //1. 打印緩存命中率 if (log.isDebugEnabled()) { log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio()); } return value; }/<code>
除此之外沒有什麼其他功能。我們主要看下他是如何統計緩存命中率的。其實很簡單。
<code>public class LoggingCache implements Cache { private final Log log; private final Cache delegate; //1. 總請求次數 protected int requests = 0; //2. 命中次數 protected int hits = 0; ...} /<code>
在get請求時候無論是否命中,都自增總請求次數( request ), 當get命中時候自增命中次數( hits )
<code>public Object getObject(Object key) { //1. 無論是否命中,都自增總請求次數( `request` ) requests++; final Object value = delegate.getObject(key); if (value != null) { //2. get命中時候自增命中次數( `hits` ) hits++; } if (log.isDebugEnabled()) { log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio()); } return value; }/<code>
然後我們看命中率怎麼算 getHitRatio()
命中率=命中次數/總請求次數
<code> private double getHitRatio() { return (double) hits / (double) requests; }/<code>
4. LruCache
LRU是Least Recently Used的縮寫,即最近最少使用。
首先我們看如何實現 LRU 策略。 它其實就是利用 LinkedHashMap來實現 LRU 策略, JDK 提供的 LinkedHashMap天然就支持 LRU 策略。 LinkedHashMap 有一個特點如果開啟LRU策略後,每次獲取到數據後,都會把數據放到最後一個節點,這樣第一個節點肯定是最近最少用的元素。
<code>public V get(Object key) { Nodee; if ((e = getNode(hash(key), key)) == null) return null; //1. 判斷是否開始LRU策略 if (accessOrder) //2. 開啟就往後面放 afterNodeAccess(e); return e.value; } /<code>
構造中先聲明LRU淘汰策略,當size()大於構造中聲明的1024就可以在每次 putObject時候將要淘汰的移除掉。這點非常的巧妙,不知道你學習到了沒 ?
5. ScheduledCache
定時刪除,設計巧妙,可以借鑑。
<code>public class ScheduledCache implements Cache { private final Cache delegate; protected long clearInterval; protected long lastClear; public ScheduledCache(Cache delegate) { this.delegate = delegate; //1. 指定多久清理一次緩存 this.clearInterval = 60 * 60 * 1000; // 1 hour //2. 設置初始值 this.lastClear = System.currentTimeMillis(); } public void setClearInterval(long clearInterval) { this.clearInterval = clearInterval; } @Override public String getId() { return delegate.getId(); } @Override public int getSize() { clearWhenStale(); return delegate.getSize(); } @Override public void putObject(Object key, Object object) { clearWhenStale(); delegate.putObject(key, object); } @Override public Object getObject(Object key) { return clearWhenStale() ? null : delegate.getObject(key); } @Override public Object removeObject(Object key) { clearWhenStale(); return delegate.removeObject(key); } @Override public void clear() { //1. 記錄最近刪除一次時間戳 lastClear = System.currentTimeMillis(); //2. 清理掉緩存信息 delegate.clear(); } @Override public ReadWriteLock getReadWriteLock() { return null; } @Override public int hashCode() { return delegate.hashCode(); } @Override public boolean equals(Object obj) { return delegate.equals(obj); } private boolean clearWhenStale() { if (System.currentTimeMillis() - lastClear > clearInterval) { clear(); return true; } return false; }}/<code>
核心代碼
- 構造中指定多久清理一次緩存(1小時)
- 設置初始值
- clearWhenStale() 核心方法
- 然後在每個方法中調用一次這段代碼,判斷是否需要清理。
<code>private boolean clearWhenStale() { //1. 當前時間 - 最後清理時間,如果大於定時刪除時間,說明要執行清理了。 if (System.currentTimeMillis() - lastClear > clearInterval) { clear(); return true; } return false; }/<code>
6. SerializedCache
從名字上看就是支持序列化的緩存,那麼我們就要問了,為啥要支持序列化?
為啥要支持序列化?
因為如果多個用戶同時共享一個數據對象時,同時都引用這一個數據對象。如果有用戶修改了這個數據對象,那麼其他用戶拿到的就是已經修改過的對象,這樣就是出現了線程不安全。
如何解決這種問題
- 加鎖當一個線程在操作時候,其他線程不允許操作
- 新生成一個對象,這樣多個線程獲取到的數據就不是一個對象了。
只看一下核心代碼
- putObject 將對象序列化成 byte[]
- getObject 將 byte[]反序列化成對象
<code>public void putObject(Object key, Object object) { if (object == null || object instanceof Serializable) { //1. 將對象序列化成byte[] delegate.putObject(key, serialize((Serializable) object)); } else { throw new CacheException("SharedCache failed to make a copy of a non-serializable object: " + object); } }private byte[] serialize(Serializable value) { try { ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(value); oos.flush(); oos.close(); return bos.toByteArray(); } catch (Exception e) { throw new CacheException("Error serializing object. Cause: " + e, e); } } public Object getObject(Object key) { Object object = delegate.getObject(key); //1. 獲取時候將byte[]反序列化成對象 return object == null ? null : deserialize((byte[]) object); } private Serializable deserialize(byte[] value) { Serializable result; try { ByteArrayInputStream bis = new ByteArrayInputStream(value); ObjectInputStream ois = new CustomObjectInputStream(bis); result = (Serializable) ois.readObject(); ois.close(); } catch (Exception e) { throw new CacheException("Error deserializing object. Cause: " + e, e); } return result; }/<code>
這種就類似於深拷貝,因為簡單的淺拷貝會出現線程安全問題,而這種辦法,因為字節在被反序列化時,會在創建一個新的對象,這個新的對象的數據和原來對象的數據一模一樣。所以說跟深拷貝一樣。
Java開發之深淺拷貝
7. SoftCache
從名字上看,Soft其實就是軟引用。軟引用就是如果內存夠,GC就不會清理內存,只有當內存不夠用了會出現OOM時候,才開始執行GC清理。
如果要看明白這個源碼首先要先了解一點垃圾回收,垃圾回收的前提是還有沒有別的地方在引用這個對象了。如果沒有別的地方在引用就可以回收了。 本類中為了阻止被回收所以聲明瞭一個變量 hardLinksToAvoidGarbageCollection, 也指定了一個將要被回收的垃圾隊列 queueOfGarbageCollectedEntries 。
這個類的主要內容是當緩存value已經被垃圾回收了,就自動把key也清理。
Mybatis 在實際中並沒有使用這個類。
<code>public class SoftCache implements Cache { private final Deque<object> hardLinksToAvoidGarbageCollection; private final ReferenceQueue<object> queueOfGarbageCollectedEntries; private final Cache delegate; private int numberOfHardLinks; public SoftCache(Cache delegate) { this.delegate = delegate; this.numberOfHardLinks = 256; this.hardLinksToAvoidGarbageCollection = new LinkedList<object>(); this.queueOfGarbageCollectedEntries = new ReferenceQueue<object>(); }} /<object>/<object>/<object>/<object>/<code>
hardLinksToAvoidGarbageCollection 硬連接,避免垃圾收集 queueOfGarbageCollectedEntries 垃圾要收集的隊列 numberOfHardLinks 硬連接數量
<code>@Override public void putObject(Object key, Object value) { //1. 清除已經被垃圾回收的key removeGarbageCollectedItems(); //2. 注意看SoftEntry(),聲明一個SoftEnty對象,指定垃圾回收後要進入的隊列 //3. 當SoftEntry中數據要被清理,會添加到類中聲明的垃圾要收集的隊列中 delegate.putObject(key, new SoftEntry(key, value, queueOfGarbageCollectedEntries)); } @Override public Object getObject(Object key) { Object result = null; @SuppressWarnings("unchecked") // assumed delegate cache is totally managed by this cache SoftReference<object> softReference = (SoftReference<object>) delegate.getObject(key); if (softReference != null) { result = softReference.get(); if (result == null) { //1. 如果數據已經沒有了,就清理這個key delegate.removeObject(key); } else { // See #586 (and #335) modifications need more than a read lock synchronized (hardLinksToAvoidGarbageCollection) { //2. 如果key存在,讀取時候加一個鎖操作,並將緩存值添加到硬連接集合中,避免垃圾回收 hardLinksToAvoidGarbageCollection.addFirst(result); //3. 構造中指定硬鏈接最大256,所以如果已經有256個key的時候回開始刪除最先添加的key if (hardLinksToAvoidGarbageCollection.size() > numberOfHardLinks) { hardLinksToAvoidGarbageCollection.removeLast(); } } } } return result; } @Override public void clear() { //執行三清 synchronized (hardLinksToAvoidGarbageCollection) { //1.清除硬鏈接隊列 hardLinksToAvoidGarbageCollection.clear(); } //2. 清除垃圾隊列 removeGarbageCollectedItems(); //3. 清除緩存 delegate.clear(); } private void removeGarbageCollectedItems() { SoftEntry sv; //清除value已經gc準備回收了,就就將key也清理掉 while ((sv = (SoftEntry) queueOfGarbageCollectedEntries.poll()) != null) { delegate.removeObject(sv.key); } }/<object>/<object>/<code>
8. SynchronizedCache
從名字看就是同步的緩存,從代碼看即所有的方法都被 synchronized修飾。
9. TransactionalCache
從名字上看就應該能隱隱感覺到跟事務有關,但是這個事務呢又不是數據庫的那個事務。只是類似而已是, 即通過 java 代碼來實現了一個暫存區域,如果事務成功就添加緩存,事務失敗就回滾掉或者說就把暫存區的信息刪除,不進入真正的緩存裡面。 這個類是比較重要的一個類,因為所謂的二級緩存就是指這個類。既然說了緩存就順便提一下一級緩存。但是說一級緩存就設計到 Mybatis架構裡面一個 Executor 執行器
所有的查詢都先從一級緩存中查詢
看到這裡不由己提一個面試題,面試官會問你知道 Mybatis 的一級緩存嗎? 一般都會說 Mybatis 的一級緩存就是 SqlSession 自帶的緩存,這麼說也對就是太籠統了,因為 SqlSession其實就是生成 Executor 而一級緩存就是裡面query方法中的 localCache。這個時候我們就要看下了 localCache 究竟是什麼? 看一下構造,突然豁然開朗。原來本篇文章講的基本就是一級緩存的實現呀。
說到這裡感覺有點跑題了,我們不是要看 TransactionalCache 的實現嗎?
clearOnCommit 為false就是這個事務已經完成了,可以從緩存中讀取數據了。
當 clearOnCommit為 true ,這個事務正在進行中呢? 來的查詢都給你返回 null , 等到 commit 提交時候在查詢就可以從緩存中取數據了。
<code>public class TransactionalCache implements Cache { private static final Log log = LogFactory.getLog(TransactionalCache.class); // 真正的緩存 private final Cache delegate; // 是否清理已經提交的實物 private boolean clearOnCommit; // 可以理解為暫存區 private final Map<object> entriesToAddOnCommit; // 緩存中沒有的key private final Set<object> entriesMissedInCache; public TransactionalCache(Cache delegate) { this.delegate = delegate; this.clearOnCommit = false; this.entriesToAddOnCommit = new HashMap<object>(); this.entriesMissedInCache = new HashSet<object>(); } @Override public String getId() { return delegate.getId(); } @Override public int getSize() { return delegate.getSize(); } @Override public Object getObject(Object key) { // 先從緩存中拿數據 Object object = delegate.getObject(key); if (object == null) { // 如果沒有添加到set集合中 entriesMissedInCache.add(key); } // 返回數據庫的數據。 if (clearOnCommit) { return null; } else { return object; } } @Override public ReadWriteLock getReadWriteLock() { return null; } @Override public void putObject(Object key, Object object) { entriesToAddOnCommit.put(key, object); } @Override public Object removeObject(Object key) { return null; } @Override public void clear() { clearOnCommit = true; entriesToAddOnCommit.clear(); } public void commit() { if (clearOnCommit) { delegate.clear(); } flushPendingEntries(); reset(); } public void rollback() { unlockMissedEntries(); reset(); } private void reset() { //1. 是否清除提交 clearOnCommit = false; //2. 暫存區清理,代表這個事務從頭開始做了,之前的清理掉 entriesToAddOnCommit.clear(); //3. 同上 entriesMissedInCache.clear(); } /** * 將暫存區的數據提交到緩存中 **/ private void flushPendingEntries() { for (Map.Entry<object> entry : entriesToAddOnCommit.entrySet()) { delegate.putObject(entry.getKey(), entry.getValue()); } //如果緩存中不包含這個key,就將key對應的value設置為默認值null for (Object entry : entriesMissedInCache) { if (!entriesToAddOnCommit.containsKey(entry)) { delegate.putObject(entry, null); } } } // 移除缺失的key,就是這個緩存中沒有的key都移除掉 private void unlockMissedEntries() { for (Object entry : entriesMissedInCache) { try { delegate.removeObject(entry); } catch (Exception e) { log.warn("Unexpected exception while notifiying a rollback to the cache adapter." + "Consider upgrading your cache adapter to the latest version. Cause: " + e); } } }}/<object>/<object>/<object>/<object>/<object>/<code>
10. WeakCache
從名字上看跟 SoftCache 有點關係,Soft引用是當內存不夠用時候才清理, 而 Weak 弱引用則相反, 只要有GC就會回收。 所以他們的類型特性並不是自己實現的,而是依賴於 Reference
程序猿升級課
閱讀更多 軟件編程指南 的文章