MyBatis 作爲一個強大的持久層框架,緩存是其必不可少的功能之一。 MyBatis 中的緩存是兩層結構的,分爲一級緩存、二級緩存,但在本質上是相同的,它們使用的都是 Cache 接 口的實現。
緩存模式主要是設計模式是裝飾器模式。
在 MyBatis 的緩存模塊中,使用了裝飾器模式的變體,其中將 Decorator 接口和 Component接口合併爲一個 Component 接口,得到的類問結構下圖所示。
使用裝飾器模式的有兩個明顯的優點:
1、相較於繼承來說,裝飾器模式的靈活性更強,可擴展性也強。正如前面所說,繼承方 式會導致大量子類的情況。而裝飾者模式可以將複雜的功能切分成一個個獨立的裝飾 器,通過多個獨立裝飾器的動態組合,創建不同功能的組件,從而滿足多種不同需求。
2、當有新功能需要添加時,只需要添加新的裝飾器實現類,然後通過組合方式添加這個 新裝飾器即可,無須修改己有類的代碼,符合“開放一封閉”原則。
但是,隨着添加的新需求越來越多,可能會創建出嵌套多層裝飾器的對象,這增加了系統 的複雜性, 也增加了理解的難度和定位錯誤的難度。
一、Cache 接口及其實現
MyBatis 中緩存模塊相關的代碼位於 cache 包下, 其中 Cache 接口是緩存模塊中最核心的接 口,它定義了所有緩存的基本行爲。 Cache 接口的定義如下:
public interface Cache {
//該緩存對象的id
String getId();
//向緩存中添加數據,一般情況下, key 是 CacheKey, value 是查詢結果
void putObject(Object key, Object value);
//根據指定的 key,在緩存中查找對應的結果對象
Object getObject(Object key);
//刪除 key 對應的緩存項
Object removeObject(Object key);
//清空緩存
void clear();
//緩存項的個數,該方法不會被 MyBatis 核心代碼使用,所以可提供空實現
int getSize();
//獲取讀寫鎖,該方法不會被 MyBatis 核心代碼使用,所以可提供空實現
default ReadWriteLock getReadWriteLock() {
return null;
}
}
Cache 接口的實現類有多個,但大部分都是裝飾器,只有 PerpetualCache 提供了 Cache 接口的基本實現。
1.1 Perpetual Cache
Perpetual Cache 在緩存模塊中扮演着 ConcreteComponent 的角色,其實現比較簡單,底層使 用 HashMap 記錄緩存項,也是通過該 HashMap 對象的方法實現的 Cache 接口中定義的相應方 法。 PerpetualCache 的具體實現如下:
public class PerpetualCache implements Cache {
//Cache的唯一標識
private final String id;
//使用MAP記錄緩存
private Map<Object, Object> cache = new HashMap<>();
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();}
// ... 重寫 了 equals ()方法和 hashCode ()方法,兩者都只關心 id 字段,並不關心 cache 字段(略)
}
下面來介紹 cache.decorators 包下提供的裝飾器,它們都直接實現了 Cache 接口,扮演着 ConcreteDecorator 的角色。這些裝飾器會在 PerpetualCache 的基礎上提供一些額外的功能,通 過多個組合後滿足一個特定的需求,後面介紹二級緩存時,會見到這些裝飾器是如何完成動態 組合的。
1.2 BlockingCache
BlockingCache 是阻塞版本的緩存裝飾器,它會保證只有一個線程到數據庫中查找指定 key 對應的數據。屬於細粒度鎖,會阻塞相同key的線程,對於不同key的線程不會阻塞。
BlockingCache 中各個字段的含義如下:
private long timeout;//阻塞的超時時長
private final Cache delegate;//被裝飾的底層對象,一般是PerpetualCache
//每個 key 都有對應的 ReentrantLock 對象
private final ConcurrentHashMap<Object, ReentrantLock> locks;//鎖對象集,粒度到key值
public BlockingCache(Cache delegate) {
this.delegate = delegate;
this.locks = new ConcurrentHashMap<>();
}
假設線程 A 在 BlockingCache 中未查找到 keyA 對應的緩存項時,線程 A 會獲取 keyA 對應 的鎖,這樣後續線程在查找 keyA 時會發生阻塞,如下圖所示:
BlockingCache.getObject()方法的代碼如下:
/**
* 細粒度鎖
* 在get的時候先去獲得key的鎖
* @param key The key
* @return
*/
@Override
public Object getObject(Object key) {
//嘗試去獲取鎖
acquireLock(key);
Object value = delegate.getObject(key);
//緩存有 key 對應的緩存項,擇放鎖,否則繼續持有鎖
if (value != null) {
//釋放鎖
releaseLock(key);
}
return value;
}
acquireLock(key)會去嘗試獲取當前key的鎖,如果當前key沒有對象鎖則爲其創建新的ReentrantLock對象,在對其加鎖。如果有改key的對象鎖存在則使用該對象,對其加鎖。如果加鎖失敗,阻塞一段時間。其代碼如下:
//acquireLock()方法中會嘗試獲取指定 key 對應的鎖。
// 如果該 key 沒有對應的鎖對象則爲其創建新的 ReentrantLock 對象,再加鎖;如果獲取鎖失敗, 則阻塞一段時間。
private void acquireLock(Object key) {
//獲取ReentrantLock 對象
Lock lock = getLockForKey(key);
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 {
lock.lock();
}
}
private ReentrantLock getLockForKey(Object key) {
//把新鎖添加到locks集合中,如果添加成功使用新鎖,如果添加失敗則使用locks集合中的鎖
return locks.computeIfAbsent(key, k -> new ReentrantLock());
}
假設線程 A 從數據庫中查找到 keyA 對應的結果對象後,將結果對象放入到 BlockingCache 中,此時線程 A 會釋放 keyA 對應的鎖,喚醒阻塞在該鎖上的線程。其他線程即可從 BlockingCache 中獲取 keyA 對應的數據,而不是再次訪問數據庫
BlockingCache.putObject()方法的實現如下:
@Override
public void putObject(Object key, Object value) {
try {
delegate.putObject(key, value);
} finally {
releaseLock(key);
}
}
//釋放鎖
private void releaseLock(Object key) {
ReentrantLock lock = locks.get(key);
//鎖是否被當前線程持有
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
1.3 FifoCache&LruCache
在很多場景中,爲了控制緩存大小,系統需要按照一定的規則清楚緩存。fifocache 是先進先出版本的裝飾器,當向緩存添加數據時,如果緩存數據個數已經達到上線,則清除最先進來的緩存數據。
FifoCache中屬性如下所示:
private final Cache delegate;//底層被裝飾的cache對象
//用於記錄key的進入順序,使用的是LinkedList類型的集合對象
private final Deque<Object> keyList;
private int size;//記錄緩存項的上線,超過該項則清楚最老的數據默認是1024
public FifoCache(Cache delegate) {
this.delegate = delegate;
this.keyList = new LinkedList<>();
this.size = 1024;
}
FifoCacbe.getObject()和 removeObject()方法的實現都是直接調用底層 Cache 對象的對應方 法, 不再贅述。 在 FifoCacbe.p舊Object()方法中會完成緩存項個數的檢測以及緩存的清理操作, 具體實現如下:
@Override
public void putObject(Object key, Object value) {
cycleKeyList(key);//把key放入隊列中,檢測並清理緩存
delegate.putObject(key, value);//記錄緩存
}
private void cycleKeyList(Object key) {
keyList.addLast(key);//記錄key
if (keyList.size() > size) {//如果緩存達到上線則清除最早記錄
Object oldestKey = keyList.removeFirst();
delegate.removeObject(oldestKey);
}
}
LruCache 是按照近期最少使用算法(Least Recently Used, LRU)進行緩存清理的裝飾器, 在需要清理緩存時,它會清除最近最少使用的緩存工頁。LruCache 中定義的各個字段的含義如下:
private final Cache delegate;//被裝飾的底層cache對象
//LinkedHashMap<Obj ect, Object>類型對象,它是一個有序的 HashMap,用於記錄 key 最近的使用情況
private Map<Object, Object> keyMap;
private Object eldestKey;//記錄最少被使用的緩存項的key
public LruCache(Cache delegate) {
this.delegate = delegate;
setSize(1024);
}
LruCache 的構造函數中默認設置的緩存大小是 1024,我們可以通過其 setSize()方法重新設 置緩存大小, 具體實現如下:
//重新設置緩存大小時,會重置 keyMap 字段
public void setSize(final int size) {
//LinkedHashMap的第三個參數爲true,true表示表示該 LinkedHashMap 記錄的順序是
// access-order,也就是說 LinkedHashMap.get()方法會改變其記錄的順序
keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
private static final long serialVersionUID = 4267176411845948333L;
//重寫方法 當調用 LinkedHashMap . put ()方法時,會調用該方法
@Override
protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
boolean tooBig = size() > size;
if (tooBig) {//如果已到達緩存上限,則更新 eldestKey 字段, 後面會刪除該項
eldestKey = eldest.getKey();
}
return tooBig;
}
};
}
爲了讓讀者更好地理解 LinkedHashMap,下圖 展示了其原理,圖中的虛線形成了一個隊 列,當 LinkedHashMap.get()方法訪問 K1時,會修改這條虛線隊列將 K1項移動到隊列尾部, LruCache 就是通過 LinkedHashMap 的這種特性來確定最久未使用的緩存項。
LruCache.getObject()方法除了返回緩存項,還會調用 keyMap.get()方法修改 key 的順序,表 示指定的 key 最近被使用。 LruCache.putObject()方法除了添加緩存項,還會將 eldestKey 宇段指 定的緩存項清除掉。具體實現如下:
@Override
public Object getObject(Object key) {
keyMap.get(key); //修改key中在記錄中的順序
return delegate.getObject(key);
}
@Override
public void putObject(Object key, Object value) {
delegate.putObject(key, value);//加入緩存
cycleKeyList(key);//把key加入記錄中並會清除緩存
}
private void cycleKeyList(Object key) {
//
keyMap.put(key, key);
if (eldestKey != null) {//eldestKey 不爲空,表示已經達到緩存上
delegate.removeObject(eldestKey);//清除最少使用的緩存
eldestKey = null;
}
}
1.4 SoftCache&WeakCache
Java 提供的 4 種引 用類型,它們分別是強引用 (Strong Reference)、軟引用 ( soft Reference)、弱引用(Weak Reference) 和幽靈引用(Phantom Reference)。詳情可以自行了解。
SoftCache的實現。SoftCache中各個字段的 含義如下:
// 在 SoftCache 中,最近使用的一部分緩存項不會被 GC 回收,
// 這就是通過將其 value 添加到 II hardLinksToAvoidGarbageCollection
// 集合中實現的(即有強引用指向其 value) II hardLinksToAvoidGarbageCollection 集合是 LinkedList<Object>類型
private final Deque<Object> hardLinksToAvoidGarbageCollection;
//ReferenceQueue,引用隊列,用於記錄已經被GC回收的緩存項所對應的SoftEntry對象
private final ReferenceQueue<Object> queueOfGarbageCollectedEntries;
private final Cache delegate;
private int numberOfHardLinks;//強連接的個數, 默認值是 256
public SoftCache(Cache delegate) {
this.delegate = delegate;
this.numberOfHardLinks = 256;
this.hardLinksToAvoidGarbageCollection = new LinkedList<>();
this.queueOfGarbageCollectedEntries = new ReferenceQueue<>();
}
SoftCache 中緩存項的 value 是 SoftEntry對象, SoftEntry 繼承了 SoftReference<Object>, 其中指向 key 的引用是強引用, 而指向 value 的引用是軟引用 。 SoftEntry對象的實現如下:
private static class SoftEntry extends SoftReference<Object> {
private final Object key;
//
SoftEntry(Object key, Object value, ReferenceQueue<Object> garbageCollectionQueue) {
super(value, garbageCollectionQueue);//指向 value 的引用是軟引用,且關聯了引用隊列
this.key = key;///key是強引用
}
}
SoftCache.putObject()方法除了向緩存中添加緩存項,還會清除己經被 GC 回收的緩存項, 其具體實現如下:
@Override
public void putObject(Object key, Object value) {
//清除已經被 GC 回收的緩存項
removeGarbageCollectedItems();
//向緩存中添加緩存項
delegate.putObject(key, new SoftEntry(key, value, queueOfGarbageCollectedEntries));
}
//下面是 removeGarbageCollecteditems()方法的實現:
private void removeGarbageCollectedItems() {
SoftEntry sv;
//遍歷queueOfGarbageCollectedEntries 集合
while ((sv = (SoftEntry) queueOfGarbageCollectedEntries.poll()) != null) {
delegate.removeObject(sv.key);//將已經被 GC 回收的 value 對象對應的緩存項清除
}
}
SoftCache.getObject()方法除了從緩存中查找對應的 value,處理被 GC 回收的 value 對應的 緩存項, 還會更新 hardLinksToAvoidGarbageCollection 集合, 具體實現如下:
@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) {//已經被 GC 回收
delegate.removeObject(key);//從緩存中清除對應的緩存項
} else {
///緩存項的 value 添加到 hardLinksToAvoidGarbageCollection 集合中保存
// See #586 (and #335) modifications need more than a read lock
synchronized (hardLinksToAvoidGarbageCollection) {
hardLinksToAvoidGarbageCollection.addFirst(result);
if (hardLinksToAvoidGarbageCollection.size() > numberOfHardLinks) {
hardLinksToAvoidGarbageCollection.removeLast();
}
}
}
}
return result;
}
1.5 ScheduledCache&LoggingCache&Synchronized&CacheSerializedCache
ScheduledCache 是週期性清理緩存的裝飾器,它的 clear Interval 宇段記錄了兩次緩存清理之 間的時間間隔,默認是一小時, lastClear 字段記錄了最近一次清理的時間戳。 ScheduledCache 的 getObject()、 putObject()、 removeObject()等核心方法在執行時,都會根據這兩個字段檢測是 否需要進行清理操作,清理操作會清空緩存中所有緩存項。
LoggingCache 在 Cache 的基礎上提供了日誌功能,它通過 hit 宇段和 request 字段記錄了 Cache 的命中次數和訪問次數。在 LoggingCache.getO均ect()方法中會統計命中次數和訪問次數 這兩個指標,井按照指定的日誌輸出方式輸出命中率。 LoggingCache 代碼比較簡單,請讀者參 考代碼學習。
SynchronizedCache通過在每個方法上添加 synchronized關鍵字,爲Cache添加了同步功能, 有點類似於 JDK 中 Collections 中的 SynchronizedCollection 內部類的實現。 SynchronizedCache 代碼比較簡單。
Serialized Cache 提供了將 value 對象序列化的功能。 S巳rializedCache 在添加緩存項時,會將 value 對應的 Java 對象進行序列化,井將序列化後的 byte[]數組作爲 value 存入緩存 。 Serialized Cache 在獲取緩存項時,會將緩存項中的 byte[]數組反序列化成 Java 對象。使用前面 介紹的 Cache 裝飾器實現進行裝飾之後,每次從緩存中獲取同- key 對應的對象時,得到的都 是同一對象,任意一個線程修改該對象都會影 響到其他線程以及緩存中的對象:而 SerializedCache 每次從緩存中獲取數據時,都會通過反序列化得到一個全新的對象。
二、CacheKey
在 Cache 中唯一確定一個緩存項需要使用緩存項的 key, MyBatis 中因爲涉及動態 SQL 等 多方面因素, 其緩存項的 key 不能僅僅通過一個 String 表示,所以 MyBatis 提供了 CacheKey 類來表示緩存項的 key,在一個 CacheKey 對象中可以封裝多個影響緩存項的因素。 CacheKey 中可以添加多個對象,由這些對象共同確定兩個 CacheKey 對象是否相同。
CacheKey中的核心字段:
private static final int DEFAULT_MULTIPLIER = 37;
private static final int DEFAULT_HASHCODE = 17;
private final int multiplier;//參與hash計算的乘數 默認是37
private int hashcode;//CacheKey的hash值,在update函數中實時運算出來的
private long checksum;//校驗和
private int count;//updateList集合的個數
private List<Object> updateList;//由該集合中的所有對象共同決定兩個 CacheKey 是否相同
public CacheKey() {
this.hashcode = DEFAULT_HASHCODE;
this.multiplier = DEFAULT_MULTIPLIER;
this.count = 0;
this.updateList = new ArrayList<>();
}
CacheKey 對象由四個部分構成,也就是說這四部分都 會記錄到該 CacheKey 對象的 updateList 集合中:
1、MappedStatement 的 id。
2、指定查詢結果集的範圍,也就是 RowBounds.offset 和 RowBounds.limit。
3、查詢所使用的 SQL 語句,也就是 boundSql.getSql()方法返回的 SQL 語句,其中可能包 含“?”佔位符。
4、用戶傳遞給上述 SQL 語句的實際參數值。
在向 CacheKey.updateList 集合中添加對象時,使用的是 CacheKey.update()方法,具體實現 如下:
public CacheKey(Object[] objects) {
this();
updateAll(objects);
}
public void updateAll(Object[] objects) {
for (Object o : objects) {
update(o);
}
}
public void update(Object object) {
//獲取object的hash值
int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
//更新count、checksum以及hashcode的值
count++;
checksum += baseHashCode;
baseHashCode *= count;
hashcode = multiplier * hashcode + baseHashCode;
//將對象添加到updateList中
updateList.add(object);
}
CacheKey 重寫了 equals()方法和 hashCode()方法,這兩個方法使用上面介紹的 count、 checksum、 hashcode、 updateList 比較 CacheKey 對象是否相同,具體實現如下:
@Override
public boolean equals(Object object) {
if (this == object) { //比較是否是同一對象
return true;
}
if (!(object instanceof CacheKey)) {//判斷是否是同一類型
return false;
}
final CacheKey cacheKey = (CacheKey) object;
if (hashcode != cacheKey.hashcode) {//判斷hashcode是否相同
return false;
}
if (checksum != cacheKey.checksum) {////checksum是否相同
return false;
}
if (count != cacheKey.count) {//count是否相同
return false;
}
//順序比較updateList中元素的hash值是否一致
for (int i = 0; i < updateList.size(); i++) {
Object thisObject = updateList.get(i);
Object thatObject = cacheKey.updateList.get(i);
if (!ArrayUtil.equals(thisObject, thatObject)) {
return false;
}
}
return true;
}
@Override
public int hashCode() {
return hashcode;
}
三 cache裝飾者類的使用
CacheBuilder.setStandardDecorators()方法會根據 CacheBuilder 中各個字段的值,爲 cache 對 象添加對應的裝飾器,具體實現如下:
//緩存模塊裝飾器的使用
private Cache setStandardDecorators(Cache cache) {
try {
//獲取基本的cache對象
MetaObject metaCache = SystemMetaObject.forObject(cache);
//緩存的容量大小
if (size != null && metaCache.hasSetter("size")) {
metaCache.setValue("size", size);
}
//檢測是否指定了 clearinterval 字段 (定期清除)
if (clearInterval != null) {
cache = new ScheduledCache(cache);//添加定期清除的裝飾器
((ScheduledCache) cache).setClearInterval(clearInterval);
}
//如果是隻讀緩存
if (readWrite) {
//是否只讀,對應添加 SerializedCache 裝飾器
cache = new SerializedCache(cache);
}
//默認添加 LoggingCache 和 SynchronizedCache 兩個裝飾器
cache = new LoggingCache(cache);
cache = new SynchronizedCache(cache);
if (blocking) {//阻塞裝飾器
cache = new BlockingCache(cache);
}
return cache;
} catch (Exception e) {
throw new CacheException("Error building standard cache decorators. Cause: " + e, e);
}
}