看Mybatis如何花樣設計 Cache
爲什麼說花樣設計 Cache
, 是因爲Mybatis
只是對 Map
數據結構的封裝, 但是卻實現了很多挺好用的能力。如果單單從設計模式上的角度來,其實就是典型的裝飾器模式, 裝飾器模式其實並不難,所以我們不講設計模式, 本篇文章我們來看看Mybatils
緩存設計巧妙的點。
通過簡單的代碼review來分析下這十個緩存類設計的巧妙點。
一、模式分析
從目錄就很清晰看出,核心就是impl
包下面只有一個,其他都是裝飾器模式,在
decorators
包下
1. Cache
接口設計沒有什麼好講的,提供獲取和添加方法,跟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();
}
2. PerpetualCache
這個類就是 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();
}
}
3. 小總結
其實上面就是Mybatis
關於 Cache
的核心實現,其實看到這裏還沒有很多知識點. 那麼我們從中能學到什麼呢? 如果真要找一條學習的點,那麼就是:
設計要面向接口設計,而不是具體實現。 這樣當我們要重寫 Cache
,比如說我們不想底層用 HashMap
來實現了,其實我們只要實現一下 Cache
接口,然後替換掉PerpetualCache
就可以了。對於使用者其實並不感知。
二、開始重頭戲
從這裏我們主要一起看下,代碼設計的巧妙之處,一個一個研究下,以下這10個類。看 Mybatis
是如何巧妙設計的。
1. BlockingCache
BlockingCache是一個簡單和低效的Cache
的裝飾器,我們主要看幾個重要方法。
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對應的鎖存在就返回,沒有就創建一個新的 |
思考
- 這個因爲每次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 來實現先進先出。
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);
}
}
3. LoggingCache
從名字上看就是跟日誌有關, 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;
}
4. LruCache
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時候將要淘汰的移除掉。這點非常的巧妙,不知道你學習到了沒 ?
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;
}
}
核心代碼
- 構造中指定多久清理一次緩存(1小時)
- 設置初始值
clearWhenStale()
核心方法- 然後在每個方法中調用一次這段代碼,判斷是否需要清理。
private boolean clearWhenStale() {
//1. 當前時間 - 最後清理時間,如果大於定時刪除時間,說明要執行清理了。
if (System.currentTimeMillis() - lastClear > clearInterval) {
clear();
return true;
}
return false;
}
6. SerializedCache
從名字上看就是支持序列化的緩存,那麼我們就要問了,爲啥要支持序列化?
爲啥要支持序列化?
因爲如果多個用戶同時共享一個數據對象時,同時都引用這一個數據對象。如果有用戶修改了這個數據對象,那麼其他用戶拿到的就是已經修改過的對象,這樣就是出現了線程不安全。
如何解決這種問題
- 加鎖當一個線程在操作時候,其他線程不允許操作
- 新生成一個對象,這樣多個線程獲取到的數據就不是一個對象了。
只看一下核心代碼
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;
}
這種就類似於深拷貝,因爲簡單的淺拷貝會出現線程安全問題,而這種辦法,因爲字節在被反序列化時,會在創建一個新的對象,這個新的對象的數據和原來對象的數據一模一樣。所以說跟深拷貝一樣。
7. SoftCache
從名字上看,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);
}
}
8. SynchronizedCache
從名字看就是同步的緩存,從代碼看即所有的方法都被synchronized
修飾。
9. TransactionalCache
從名字上看就應該能隱隱感覺到跟事務有關,但是這個事務呢又不是數據庫的那個事務。只是類似而已是, 即通過 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);
}
}
}
}
10. WeakCache
從名字上看跟 SoftCache
有點關係,Soft引用是當內存不夠用時候才清理, 而Weak
弱引用則相反, 只要有GC就會回收。 所以他們的類型特性並不是自己實現的,而是依賴於 Reference<T>
類的特性,所以代碼就不看了基本和 SoftCache
實現一摸一樣。
感謝您的閱讀,本文由 程序猿升級課 版權所有。如若轉載,請註明出處:程序猿升級課(https://blog.springlearn.cn/)