Cache
爲何說花樣設計 Cache
, 是由於Mybatis
只是對 Map
數據結構的封裝, 可是卻實現了不少挺好用的能力。若是單單從設計模式上的角度來,其實就是典型的裝飾器模式, 裝飾器模式其實並不難,因此咱們不講設計模式, 本篇文章咱們來看看Mybatils
緩存設計巧妙的點。 java
經過簡單的代碼review來分析下這十個緩存類設計的巧妙點。面試
從目錄就很清晰看出,核心就是impl
包下面只有一個,其餘都是裝飾器模式,在decorators
包下spring
接口設計沒有什麼好講的,提供獲取和添加方法,跟Map接口同樣。 本篇咱們要一塊兒Review的類都會實現該接口的。數據庫
(這句話簡直就是廢話,大佬勿噴,就是簡單提醒。意思就是其實代碼不難)設計模式
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(); }
這個類就是 Mybatis
緩存最底層的設計, 看一下就知道實際上是對 Map
的封裝。
其實咱們只要知道他是簡單的 HashMap
的封裝就能夠了緩存
public class PerpetualCache implements Cache { // 惟一標識 private final String id; // 就是一個HashMap結構 private Map<Object, Object> cache = new HashMap<Object, 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(); } }
其實上面就是Mybatis
關於 Cache
的核心實現,其實看到這裏尚未不少知識點. 那麼咱們從中能學到什麼呢? 若是真要找一條學習的點,那麼就是:安全
設計要面向接口設計,而不是具體實現。 這樣當咱們要重寫 Cache
,好比說咱們不想底層用 HashMap
來實現了,其實咱們只要實現一下 Cache
接口,而後替換掉PerpetualCache
就能夠了。對於使用者其實並不感知。數據結構
從這裏咱們主要一塊兒看下,代碼設計的巧妙之處,一個一個研究下,如下這10個類。看 Mybatis
是如何巧妙設計的。架構
BlockingCache是一個簡單和低效的Cache
的裝飾器,咱們主要看幾個重要方法。ide
public class BlockingCache implements Cache { private long timeout; //實現Cache接口的緩存對象 private final Cache delegate; //對每一個key生成一個鎖對象 private final ConcurrentHashMap<Object, ReentrantLock> locks; public BlockingCache(Cache delegate) { this.delegate = delegate; this.locks = new ConcurrentHashMap<Object, ReentrantLock>(); } @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; } }
建議看代碼註釋
方法 | 解釋 |
---|---|
acquireLock | 加鎖操做 |
getObject | 進來加鎖,若是緩存存在就釋放鎖,不存在就不釋放鎖。 |
putObject | 添加元素並釋放鎖 |
removeObject | 移除key的時候,順便清楚緩存key的鎖對象 |
getLockForKey | 若是key對應的鎖存在就返回,沒有就建立一個新的 |
思考
注意這個加lock並非對get方法加lock,而是對每一個要get的key來加lock。
場景一: 試想一種場景,當有10個線程同時從數據庫查詢一個key爲123的數據時候,當第一個線程來首先從cache中讀取時候,這個時候其餘九個線程是會阻塞的,由於這個key已經被加lock了。當第一個線程get這個key完成時候,其餘線程才能繼續走。這種場景來講是很差的,
場景二: 可是當第一個線程來發現cache裏面沒有數據這個時候其餘線程會阻塞,而第一個線程會從db中查詢,而後在put到cache裏面。這樣其餘9個線程就不須要在去查詢db了,就減小了9次db查詢。
FIFO( First Input First Output),簡單說就是指先進先出
如何實現先進先出呢? 其實很是簡單,當put時候,先判斷是否須要執行淘汰策略,若是要執行淘汰,就 移除先進來的。 直接經過 Deque
API 來實現先進先出。
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); } }
從名字上看就是跟日誌有關, LoggingCache
會在 debug
級別下把緩存命中率給統計出來,而後經過日誌系統打印出來。
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; }
除此以外沒有什麼其餘功能。咱們主要看下他是如何統計緩存命中率的。其實很簡單。
public class LoggingCache implements Cache { private final Log log; private final Cache delegate; //1. 總請求次數 protected int requests = 0; //2. 命中次數 protected int hits = 0; ... }
在get請求時候不管是否命中,都自增總請求次數( request
), 當get命中時候自增命中次數( hits
)
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; }
而後咱們看命中率怎麼算 getHitRatio()
命中率 = 命中次數 / 總請求次數
private double getHitRatio() { return (double) hits / (double) requests; }
LRU是Least Recently Used的縮寫,即最近最少使用。
首先咱們看如何實現 LRU
策略。
它其實就是利用 LinkedHashMap
來實現 LRU
策略, JDK
提供的 LinkedHashMap
自然就支持 LRU
策略。LinkedHashMap
有一個特色若是開啓LRU策略後,每次獲取到數據後,都會把數據放到最後一個節點,這樣第一個節點確定是最近最少用的元素。
public V get(Object key) { Node<K,V> e; if ((e = getNode(hash(key), key)) == null) return null; //1. 判斷是否開始LRU策略 if (accessOrder) //2. 開啓就日後面放 afterNodeAccess(e); return e.value; }
構造中先聲明LRU淘汰策略,當size()大於構造中聲明的1024就能夠在每次
putObject時候將要淘汰的移除掉。這點很是的巧妙,不知道你學習到了沒 ?
定時刪除,設計巧妙,能夠借鑑。
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; } }
核心代碼
clearWhenStale()
核心方法private boolean clearWhenStale() { //1. 當前時間 - 最後清理時間,若是大於定時刪除時間,說明要執行清理了。 if (System.currentTimeMillis() - lastClear > clearInterval) { clear(); return true; } return false; }
從名字上看就是支持序列化的緩存,那麼咱們就要問了,爲啥要支持序列化?
爲啥要支持序列化?
由於若是多個用戶同時共享一個數據對象時,同時都引用這一個數據對象。若是有用戶修改了這個數據對象,那麼其餘用戶拿到的就是已經修改過的對象,這樣就是出現了線程不安全。
如何解決這種問題
只看一下核心代碼
putObject
將對象序列化成byte[]
getObject
將byte[]
反序列化成對象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; }
這種就相似於深拷貝,由於簡單的淺拷貝會出現線程安全問題,而這種辦法,由於字節在被反序列化時,會在建立一個新的對象,這個新的對象的數據和原來對象的數據如出一轍。因此說跟深拷貝同樣。
從名字上看,Soft其實就是軟引用。軟引用就是若是內存夠,GC就不會清理內存,只有當內存不夠用了會出現OOM時候,纔開始執行GC清理。
若是要看明白這個源碼首先要先了解一點垃圾回收,垃圾回收的前提是還有沒有別的地方在引用這個對象了。若是沒有別的地方在引用就能夠回收了。
本類中爲了阻止被回收因此聲明瞭一個變量hardLinksToAvoidGarbageCollection
,
也指定了一個將要被回收的垃圾隊列queueOfGarbageCollectedEntries
。
這個類的主要內容是當緩存value已經被垃圾回收了,就自動把key也清理。
Mybatis
在實際中並無使用這個類。
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>(); } }
先看下變量聲明
hard Links To Avoid Garbage Collection
硬鏈接,避免垃圾收集queue Of Garbage Collected Entries
垃圾要收集的隊列number Of Hard Links
硬鏈接數量
@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); } }
從名字看就是同步的緩存,從代碼看即全部的方法都被synchronized
修飾。
從名字上看就應該能隱隱感受到跟事務有關,可是這個事務呢又不是數據庫的那個事務。只是相似而已經是, 即經過 java
代碼來實現了一個暫存區域,若是事務成功就添加緩存,事務失敗就回滾掉或者說就把暫存區的信息刪除,不進入真正的緩存裏面。 這個類是比較重要的一個類,由於所謂的二級緩存就是指這個類。既然說了🎧緩存就順便提一下一級緩存。可是說一級緩存就設計到 Mybatis
架構裏面一個 Executor
執行器
全部的查詢都先從一級緩存中查詢
看到這裏不禁己提一個面試題,面試官會問你知道Mybatis
的一級緩存嗎?
通常都會說Mybatis
的一級緩存就是 SqlSession
自帶的緩存,這麼說也對就是太籠統了,由於 SqlSession
其實就是生成 Executor
而一級緩存就是裏面query方法中的 localCache
。這個時候咱們就要看下了localCache
到底是什麼?
看一下構造,忽然豁然開朗。原來本篇文章講的基本就是一級緩存的實現呀。
說到這裏感受有點跑題了,咱們不是要看 TransactionalCache
的實現嗎?
clearOnCommit
爲false就是這個事務已經完成了,能夠從緩存中讀取數據了。
當clearOnCommit
爲 true
,這個事務正在進行中呢? 來的查詢都給你返回 null
, 等到 commit
提交時候在查詢就能夠從緩存中取數據了。
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, Object> entriesToAddOnCommit; // 緩存中沒有的key private final Set<Object> entriesMissedInCache; public TransactionalCache(Cache delegate) { this.delegate = delegate; this.clearOnCommit = false; this.entriesToAddOnCommit = new HashMap<Object, 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, 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); } } } }
從名字上看跟 SoftCache
有點關係,Soft引用是當內存不夠用時候才清理, 而Weak
弱引用則相反, 只要有GC就會回收。 因此他們的類型特性並非本身實現的,而是依賴於 Reference<T>
類的特性,因此代碼就不看了基本和 SoftCache
實現一摸同樣。
感謝您的閱讀,本文由 程序猿升級課 版權全部。如若轉載,請註明出處: 程序猿升級課(https://blog.springlearn.cn/)