MyBatis 對緩存的設計是非常巧妙的软免》【花樣很多,但卻不是真的花樣植兰。因為
Mybatis
只是對Map
數(shù)據(jù)結(jié)構(gòu)的封裝, 但是卻實現(xiàn)了很多挺好用的能力份帐。
如果單單從設計模式上的角度來,其實就是典型的裝飾器模式, 裝飾器模式其實并不難,所以我們不講設計模式, 本篇文章我們來看看Mybatils
緩存設計巧妙的點。
下面通過簡單的代碼review來分析下這11個緩存類設計的巧妙點楣导。(因為是對博客重構(gòu),歷史圖片就沒有補充,圖上只有10個)
一废境、模式分析
從目錄就很清晰看出,核心就是
impl
包下面只有一個,其他都是裝飾器模式,在decorators
包下
:::tip
其實上面就是Mybatis
關(guān)于 Cache
的核心實現(xiàn),其實看到這里還沒有很多知識點. 那么我們從中能學到什么呢? 如果真要找一條學習的點,那么就是:
設計要面向接口設計筒繁,而不是具體實現(xiàn)噩凹。 這樣當我們要重寫 Cache
,比如說我們不想底層用 HashMap
來實現(xiàn)了,其實我們只要實現(xiàn)一下 Cache
接口膝晾,然后替換掉PerpetualCache
就可以了栓始。對于使用者其實并不感知。
:::
1.1 Cache
接口設計沒有什么好講的血当,提供獲取和添加方法幻赚,跟Map接口一樣。 本篇我們要一起Review的類都會實現(xiàn)該接口的臊旭。
(這句話簡直就是廢話,大佬勿噴,就是簡單提醒落恼。意思就是其實代碼不難)
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();
}
1.2 PerpetualCache
這個類就是 Mybatis
緩存最底層的設計, 看一下就知道其實是對 Map
的封裝。
其實我們只要知道他是簡單的 HashMap
的封裝就可以了离熏。因為代碼實戰(zhàn)是太簡單了,沒啥分析的佳谦。
public class PerpetualCache implements Cache {
// 唯一標識
private final String id;
// 就是一個HashMap結(jié)構(gòu)
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();
}
}
二、開始重頭戲
從這里我們主要一起看下,代碼設計的巧妙之處,一個一個研究下,以下這10個類滋戳∽昝铮看 Mybatis
是如何巧妙設計的。
2.1 BlockingCache
BlockingCache是一個簡單和低效的Cache
的裝飾器,我們主要看幾個重要方法奸鸯。
public class BlockingCache implements Cache {
private long timeout;
//實現(xiàn)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加鎖,如果沒有就查詢數(shù)據(jù)庫然后put釋放鎖娄涩,然后其他線程就可以直接用緩存數(shù)據(jù)了窗怒。
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對應的鎖存在就返回,沒有就創(chuàng)建一個新的
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. 如果沒有設置,直接就加鎖(如果這個鎖已經(jīng)被人用了,那么就一直阻塞這里。等待上一個釋放鎖)
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對應的鎖存在就返回,沒有就創(chuàng)建一個新的 |
思考
- 這個因為每次key請求都會加lock真的會很慢嗎? 我們舉兩種場景扬虚。
注意這個加lock并不是對get方法加lock,而是對每個要get的key來加lock努隙。
場景一: 試想一種場景,當有10個線程同時從數(shù)據(jù)庫查詢一個key為123的數(shù)據(jù)時候,當?shù)谝粋€線程來首先從cache中讀取時候辜昵,這個時候其他九個線程是會阻塞的荸镊,因為這個key已經(jīng)被加lock了。當?shù)谝粋€線程get這個key完成時候堪置,其他線程才能繼續(xù)走贷洲。這種場景來說是不好的,
場景二: 但是當?shù)谝粋€線程來發(fā)現(xiàn)cache里面沒有數(shù)據(jù)這個時候其他線程會阻塞晋柱,而第一個線程會從db中查詢,然后在put到cache里面诵叁。這樣其他9個線程就不需要在去查詢db了,就減少了9次db查詢雁竞。
2.2 FifoCache
FIFO( First Input First Output),簡單說就是指先進先出
如何實現(xiàn)先進先出呢? 其實非常簡單,當put時候,先判斷是否需要執(zhí)行淘汰策略,如果要執(zhí)行淘汰,就 移除先進來的。 直接通過 Deque
API 來實現(xiàn)先進先出拧额。
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);
}
}
2.3 LoggingCache
從名字上看就是跟日志有關(guān), LoggingCache
會在 debug
級別下把緩存命中率給統(tǒng)計出來,然后通過日志系統(tǒng)打印出來侥锦。
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;
}
除此之外沒有什么其他功能进栽。我們主要看下他是如何統(tǒng)計緩存命中率的。其實很簡單恭垦。
public class LoggingCache implements Cache {
private final Log log;
private final Cache delegate;
//1. 總請求次數(shù)
protected int requests = 0;
//2. 命中次數(shù)
protected int hits = 0;
...
}
在get請求時候無論是否命中,都自增總請求次數(shù)( request
), 當get命中時候自增命中次數(shù)( hits
)
public Object getObject(Object key) {
//1. 無論是否命中,都自增總請求次數(shù)( `request` )
requests++;
final Object value = delegate.getObject(key);
if (value != null) {
//2. get命中時候自增命中次數(shù)( `hits` )
hits++;
}
if (log.isDebugEnabled()) {
log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio());
}
return value;
}
然后我們看命中率怎么算 getHitRatio()
命中率 = 命中次數(shù) / 總請求次數(shù)
private double getHitRatio() {
return (double) hits / (double) requests;
}
2.4 LruCache
LRU是Least Recently Used的縮寫快毛,即最近最少使用。
首先我們看如何實現(xiàn) LRU
策略番挺。
它其實就是利用 LinkedHashMap
來實現(xiàn) LRU
策略, JDK
提供的 LinkedHashMap
天然就支持 LRU
策略唠帝。
LinkedHashMap
有一個特點如果開啟LRU策略后,每次獲取到數(shù)據(jù)后,都會把數(shù)據(jù)放到最后一個節(jié)點,這樣第一個節(jié)點肯定是最近最少用的元素玄柏。
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;
}
構(gòu)造中先聲明LRU淘汰策略,當size()大于構(gòu)造中聲明的1024就可以在每次
putObject時候?qū)⒁蕴囊瞥艚笏ァ_@點非常的巧妙,不知道你學習到了沒 ?
2.5 ScheduledCache
定時刪除,設計巧妙,可以借鑒。
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;
}
}
核心代碼
- 構(gòu)造中指定多久清理一次緩存(1小時)
- 設置初始值
-
clearWhenStale()
核心方法 - 然后在每個方法中調(diào)用一次這段代碼,判斷是否需要清理粪摘。
private boolean clearWhenStale() {
//1. 當前時間 - 最后清理時間,如果大于定時刪除時間,說明要執(zhí)行清理了瀑晒。
if (System.currentTimeMillis() - lastClear > clearInterval) {
clear();
return true;
}
return false;
}
2.6 SerializedCache
從名字上看就是支持序列化的緩存,那么我們就要問了,為啥要支持序列化?
為啥要支持序列化?
因為如果多個用戶同時共享一個數(shù)據(jù)對象時徘意,同時都引用這一個數(shù)據(jù)對象苔悦。如果有用戶修改了這個數(shù)據(jù)對象,那么其他用戶拿到的就是已經(jīng)修改過的對象映砖,這樣就是出現(xiàn)了線程不安全间坐。
如何解決這種問題
- 加鎖當一個線程在操作時候,其他線程不允許操作
- 新生成一個對象,這樣多個線程獲取到的數(shù)據(jù)就不是一個對象了。
只看一下核心代碼
-
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. 獲取時候?qū)yte[]反序列化成對象
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;
}
這種就類似于深拷貝,因為簡單的淺拷貝會出現(xiàn)線程安全問題,而這種辦法,因為字節(jié)在被反序列化時,會在創(chuàng)建一個新的對象竹宋,這個新的對象的數(shù)據(jù)和原來對象的數(shù)據(jù)一模一樣劳澄。所以說跟深拷貝一樣。
2.7 SoftCache
從名字上看,Soft其實就是軟引用蜈七。軟引用就是如果內(nèi)存夠,GC就不會清理內(nèi)存,只有當內(nèi)存不夠用了會出現(xiàn)OOM時候,才開始執(zhí)行GC清理秒拔。
如果要看明白這個源碼首先要先了解一點垃圾回收,垃圾回收的前提是還有沒有別的地方在引用這個對象了。如果沒有別的地方在引用就可以回收了飒硅。
本類中為了阻止被回收所以聲明了一個變量hardLinksToAvoidGarbageCollection
砂缩,
也指定了一個將要被回收的垃圾隊列queueOfGarbageCollectedEntries
。
這個類的主要內(nèi)容是當緩存value已經(jīng)被垃圾回收了三娩,就自動把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
硬連接數(shù)量
@Override
public void putObject(Object key, Object value) {
//1. 清除已經(jīng)被垃圾回收的key
removeGarbageCollectedItems();
//2. 注意看SoftEntry(),聲明一個SoftEnty對象,指定垃圾回收后要進入的隊列
//3. 當SoftEntry中數(shù)據(jù)要被清理,會添加到類中聲明的垃圾要收集的隊列中
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. 如果數(shù)據(jù)已經(jīng)沒有了,就清理這個key
delegate.removeObject(key);
} else {
// See #586 (and #335) modifications need more than a read lock
synchronized (hardLinksToAvoidGarbageCollection) {
//2. 如果key存在,讀取時候加一個鎖操作,并將緩存值添加到硬連接集合中,避免垃圾回收
hardLinksToAvoidGarbageCollection.addFirst(result);
//3. 構(gòu)造中指定硬鏈接最大256,所以如果已經(jīng)有256個key的時候回開始刪除最先添加的key
if (hardLinksToAvoidGarbageCollection.size() > numberOfHardLinks) {
hardLinksToAvoidGarbageCollection.removeLast();
}
}
}
}
return result;
}
@Override
public void clear() {
//執(zhí)行三清
synchronized (hardLinksToAvoidGarbageCollection) {
//1.清除硬鏈接隊列
hardLinksToAvoidGarbageCollection.clear();
}
//2. 清除垃圾隊列
removeGarbageCollectedItems();
//3. 清除緩存
delegate.clear();
}
private void removeGarbageCollectedItems() {
SoftEntry sv;
//清除value已經(jīng)gc準備回收了,就就將key也清理掉
while ((sv = (SoftEntry) queueOfGarbageCollectedEntries.poll()) != null) {
delegate.removeObject(sv.key);
}
}
2.8 SynchronizedCache
從名字看就是同步的緩存,從代碼看即所有的方法都被synchronized
修飾雀监。
2.9 TransactionalCache
從名字上看就應該能隱隱感覺到跟事務有關(guān),但是這個事務呢又不是數(shù)據(jù)庫的那個事務双吆。只是類似而已是, 即通過 java
代碼來實現(xiàn)了一個暫存區(qū)域,如果事務成功就添加緩存,事務失敗就回滾掉或者說就把暫存區(qū)的信息刪除,不進入真正的緩存里面会前。 這個類是比較重要的一個類,因為所謂的二級緩存就是指這個類好乐。既然說了??緩存就順便提一下一級緩存。但是說一級緩存就設計到 Mybatis
架構(gòu)里面一個 Executor
執(zhí)行器
所有的查詢都先從一級緩存中查詢
看到這里不由己提一個面試題,面試官會問你知道Mybatis
的一級緩存嗎?
一般都會說Mybatis
的一級緩存就是 SqlSession
自帶的緩存,這么說也對就是太籠統(tǒng)了瓦宜,因為 SqlSession
其實就是生成 Executor
而一級緩存就是里面query方法中的 localCache
蔚万。這個時候我們就要看下了localCache
究竟是什么?
看一下構(gòu)造,突然豁然開朗。原來本篇文章講的基本就是一級緩存的實現(xiàn)呀临庇。
說到這里感覺有點跑題了反璃,我們不是要看 TransactionalCache
的實現(xiàn)嗎?
clearOnCommit
為false就是這個事務已經(jīng)完成了,可以從緩存中讀取數(shù)據(jù)了。
當clearOnCommit
為 true
,這個事務正在進行中呢? 來的查詢都給你返回 null
, 等到 commit
提交時候在查詢就可以從緩存中取數(shù)據(jù)了假夺。
public class TransactionalCache implements Cache {
private static final Log log = LogFactory.getLog(TransactionalCache.class);
// 真正的緩存
private final Cache delegate;
// 是否清理已經(jīng)提交的實物
private boolean clearOnCommit;
// 可以理解為暫存區(qū)
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) {
// 先從緩存中拿數(shù)據(jù)
Object object = delegate.getObject(key);
if (object == null) {
// 如果沒有添加到set集合中
entriesMissedInCache.add(key);
}
// 返回數(shù)據(jù)庫的數(shù)據(jù)版扩。
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. 暫存區(qū)清理,代表這個事務從頭開始做了,之前的清理掉
entriesToAddOnCommit.clear();
//3. 同上
entriesMissedInCache.clear();
}
/**
* 將暫存區(qū)的數(shù)據(jù)提交到緩存中
**/
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);
}
}
}
}
2.10 WeakCache
從名字上看跟 SoftCache
有點關(guān)系,Soft引用是當內(nèi)存不夠用時候才清理, 而Weak
弱引用則相反, 只要有GC就會回收侄泽。 所以他們的類型特性并不是自己實現(xiàn)的礁芦,而是依賴于 Reference<T>
類的特性,所以代碼就不看了基本和 SoftCache
實現(xiàn)一摸一樣悼尾。
感謝您的閱讀柿扣,本文由 西魏陶淵明 版權(quán)所有。如若轉(zhuǎn)載闺魏,請注明出處:西魏陶淵明(https://blog.springlearn.cn/)
本文由mdnice多平臺發(fā)布