![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5COwI2YmFGN3MDNihzN2EWZ0ATOyQDO5EWOmdzM2gTN08CX0JXZ252bj91Ztl2Lc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
作者:AmyliaY
出自:Doocs開源社群
原文:my.oschina.net/doocs/blog/4549852
MyBatis 中的緩存分為一級緩存、二級緩存,但在本質上是相同的,它們使用的都是 Cache 接口的實作。在這篇文章裡,我們就來分析 Cache 接口以及多個實作類的具體實作。
1、Cache 元件
MyBatis 中緩存子產品相關的代碼位于 org.apache.ibatis.cache 包 下,其中 Cache 接口 是緩存子產品中最核心的接口,它定義了所有緩存的基本行為。
public interface Cache {
/**
* 擷取目前緩存的 Id
*/
String getId();
/**
* 存入緩存的 key 和 value,key 一般為 CacheKey對象
*/
void putObject(Object key, Object value);
/**
* 根據 key 擷取緩存值
*/
Object getObject(Object key);
/**
* 删除指定的緩存項
*/
Object removeObject(Object key);
/**
* 清空緩存
*/
void clear();
/**
* 擷取緩存的大小
*/
int getSize();
/**
* !!!!!!!!!!!!!!!!!!!!!!!!!!
* 擷取讀寫鎖,可以看到,這個接口方法提供了預設的實作!!
* 這是 Java8 的新特性!!隻是平時開發時很少用到!!!
* !!!!!!!!!!!!!!!!!!!!!!!!!!
*/
default ReadWriteLock getReadWriteLock() {
return null;
}
}
如下圖所示,Cache 接口 的實作類有很多,但大部分都是裝飾器,隻有 PerpetualCache 提供了 Cache 接口 的基本實作。
PerpetualCache
PerpetualCache(Perpetual:永恒的,持續的)在緩存子產品中扮演着被裝飾的角色,其實作比較簡單,底層使用 HashMap 記錄緩存項,也是通過該 HashMap 對象 的方法實作的 Cache 接口 中定義的相應方法。
public class PerpetualCache implements Cache {
// Cache對象 的唯一辨別
private final String id;
// 其所有的緩存功能實作,都是基于 JDK 的 HashMap 提供的方法
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();
}
/**
* 其重寫了 Object 中的 equals() 和 hashCode()方法,兩者都隻關心 id字段
*/
@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();
}
}
下面來看一下 cache.decorators 包 下提供的裝飾器,它們都直接實作了 Cache 接口,扮演着裝飾器的角色。這些裝飾器會在 PerpetualCache 的基礎上提供一些額外的功能,通過多個組合後滿足一個特定的需求。
BlockingCache
BlockingCache 是阻塞版本的緩存裝飾器,它會保證隻有一個線程到資料庫中查找指定 key 對應的資料。
public class BlockingCache implements Cache {
// 阻塞逾時時長
private long timeout;
// 持有的被裝飾者
private final Cache delegate;
// 每個 key 都有其對應的 ReentrantLock鎖對象
private final ConcurrentHashMap<Object, ReentrantLock> locks;
// 初始化 持有的持有的被裝飾者 和 鎖集合
public BlockingCache(Cache delegate) {
this.delegate = delegate;
this.locks = new ConcurrentHashMap<>();
}
}
假設 線程 A 在 BlockingCache 中未查找到 keyA 對應的緩存項時,線程 A 會擷取 keyA 對應的鎖,這樣,線程 A 在後續查找 keyA 時,其它線程會被阻塞。
// 根據 key 擷取鎖對象,然後上鎖
private void acquireLock(Object key) {
// 擷取 key 對應的鎖對象
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) {
// Java8 新特性,Map系列類 中新增的方法
// V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
// 表示,若 key 對應的 value 為空,則将第二個參數的傳回值存入該 Map集合 并傳回
return locks.computeIfAbsent(key, k -> new ReentrantLock());
}
假設 線程 A 從資料庫中查找到 keyA 對應的結果對象後,将結果對象放入到 BlockingCache 中,此時 線程 A 會釋放 keyA 對應的鎖,喚醒阻塞在該鎖上的線程。其它線程即可從 BlockingCache 中擷取 keyA 對應的資料,而不是再次通路資料庫。
@Override
public void putObject(Object key, Object value) {
try {
// 存入 key 和其對應的緩存項
delegate.putObject(key, value);
} finally {
// 最後釋放鎖
releaseLock(key);
}
}
private void releaseLock(Object key) {
ReentrantLock lock = locks.get(key);
// 鎖是否被目前線程持有
if (lock.isHeldByCurrentThread()) {
// 是,則釋放鎖
lock.unlock();
}
}
FifoCache 和 LruCache
在很多場景中,為了控制緩存的大小,系統需要按照一定的規則清理緩存。FifoCache 是先入先出版本的裝飾器,當向緩存添加資料時,如果緩存項的個數已經達到上限,則會将緩存中最老(即最早進入緩存)的緩存項删除。
public class FifoCache implements Cache {
// 被裝飾對象
private final Cache delegate;
// 用一個 FIFO 的隊列記錄 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;
}
@Override
public String getId() {
return delegate.getId();
}
@Override
public int getSize() {
return delegate.getSize();
}
public void setSize(int size) {
this.size = size;
}
@Override
public void putObject(Object key, Object value) {
// 存儲緩存項之前,先在 keyList 中注冊
cycleKeyList(key);
// 存儲緩存項
delegate.putObject(key, value);
}
private void cycleKeyList(Object key) {
// 在 keyList隊列 中注冊要添加的 key
keyList.addLast(key);
// 如果注冊這個 key 會超出容積上限,則把最老的一個緩存項清除掉
if (keyList.size() > size) {
Object oldestKey = keyList.removeFirst();
delegate.removeObject(oldestKey);
}
}
@Override
public Object getObject(Object key) {
return delegate.getObject(key);
}
@Override
public Object removeObject(Object key) {
return delegate.removeObject(key);
}
// 除了清理緩存項,還要清理 key 的注冊清單
@Override
public void clear() {
delegate.clear();
keyList.clear();
}
}
LruCache 是按照"近期最少使用算法"(Least Recently Used, LRU)進行緩存清理的裝飾器,在需要清理緩存時,它會清除最近最少使用的緩存項。
public class LruCache implements Cache {
// 被裝飾者
private final Cache delegate;
// 這裡使用的是 LinkedHashMap,它繼承了 HashMap,但它的元素是有序的
private Map<Object, Object> keyMap;
// 最近最少被使用的緩存項的 key
private Object eldestKey;
// 國際慣例,構造方法中進行屬性初始化
public LruCache(Cache delegate) {
this.delegate = delegate;
// 這裡初始化了 keyMap,并定義了 eldestKey 的取值規則
setSize(1024);
}
public void setSize(final int size) {
// 初始化 keyMap,同時指定該 Map 的初始容積及加載因子,第三個參數true 表示 該LinkedHashMap
// 記錄的順序是 accessOrder,即,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;
}
};
}
// 存儲緩存項
@Override
public void putObject(Object key, Object value) {
delegate.putObject(key, value);
// 記錄緩存項的 key,超出容量則清除最久未使用的緩存項
cycleKeyList(key);
}
private void cycleKeyList(Object key) {
keyMap.put(key, key);
// eldestKey 不為空,則表示已經達到緩存上限
if (eldestKey != null) {
// 清除最久未使用的緩存
delegate.removeObject(eldestKey);
// 制空
eldestKey = null;
}
}
@Override
public Object getObject(Object key) {
// 通路 key元素 會改變該元素在 LinkedHashMap 中的順序
keyMap.get(key); //touch
return delegate.getObject(key);
}
@Override
public String getId() {
return delegate.getId();
}
@Override
public int getSize() {
return delegate.getSize();
}
@Override
public Object removeObject(Object key) {
return delegate.removeObject(key);
}
@Override
public void clear() {
delegate.clear();
keyMap.clear();
}
}
SoftCache 和 WeakCache
在分析 SoftCache 和 WeakCache 實作之前,我們再溫習一下 Java 提供的 4 種引用類型,強引用 StrongReference、軟引用 SoftReference、弱引用 WeakReference 和虛引用 PhantomReference。
•強引用 平時用的最多的,如 Object obj = new Object(),建立的 Object 對象 就是被強引用的。如果一個對象被強引用,即使是 JVM 記憶體空間不足,要抛出 OutOfMemoryError 異常,GC 也絕不會回收該對象。•軟引用 僅次于強引用的一種引用,它使用類 SoftReference 來表示。當 JVM 記憶體不足時,GC 會回收那些隻被軟引用指向的對象,進而避免記憶體溢出。軟引用适合引用那些可以通過其他方式恢複的對象,例如, 資料庫緩存中的對象就可以從資料庫中恢複,是以軟引用可以用來實作緩存,下面要介紹的 SoftCache 就是通過軟引用實作的。
另外,由于在程式使用軟引用之前的某個時刻,其所指向的對象可能己經被 GC 回收掉了,是以通過 Reference.get()方法 來擷取軟引用所指向的對象時,總是要通過檢查該方法傳回值是否為 null,來判斷被軟引用的對象是否還存活。•弱引用 弱引用使用 WeakReference 表示,它不會阻止所引用的對象被 GC 回收。在 JVM 進行垃圾回收時,如果指向一個對象的所有引用都是弱引用,那麼該對象會被回收。是以,隻被弱引用所指向的對象,其生存周期是 兩次 GC 之間 的這段時間,而隻被軟引用所指向的對象可以經曆多次 GC,直到出現記憶體緊張的情況才被回收。•虛引用 最弱的一種引用類型,由類 PhantomReference 表示。虛引用可以用來實作比較精細的記憶體使用控制,但很少使用。•引用隊列(ReferenceQueue ) 很多場景下,我們的程式需要在一個對象被 GC 時得到通知,引用隊列就是用于收集這些資訊的隊列。在建立 SoftReference 對象 時,可以為其關聯一個引用隊列,當 SoftReference 所引用的對象被 GC 時, JVM 就會将該 SoftReference 對象 添加到與之關聯的引用隊列中。當需要檢測這些通知資訊時,就可以從引用隊列中擷取這些 SoftReference 對象。不僅是 SoftReference,弱引用和虛引用都可以關聯相應的隊列。
現在來看一下 SoftCache 的具體實作。
public class SoftCache implements Cache {
// 這裡使用了 LinkedList 作為容器,在 SoftCache 中,最近使用的一部分緩存項不會被 GC
// 這是通過将其 value 添加到 hardLinksToAvoidGarbageCollection集合 實作的(即,有強引用指向其value)
private final Deque<Object> hardLinksToAvoidGarbageCollection;
// 引用隊列,用于記錄已經被 GC 的緩存項所對應的 SoftEntry對象
private final ReferenceQueue<Object> queueOfGarbageCollectedEntries;
// 持有的被裝飾者
private final Cache delegate;
// 強連接配接的個數,預設為 256
private int numberOfHardLinks;
// 構造方法進行屬性的初始化
public SoftCache(Cache delegate) {
this.delegate = delegate;
this.numberOfHardLinks = 256;
this.hardLinksToAvoidGarbageCollection = new LinkedList<>();
this.queueOfGarbageCollectedEntries = new ReferenceQueue<>();
}
private static class SoftEntry extends SoftReference<Object> {
private final Object key;
SoftEntry(Object key, Object value, ReferenceQueue<Object> garbageCollectionQueue) {
// 指向 value 的引用是軟引用,并且關聯了 引用隊列
super(value, garbageCollectionQueue);
// 強引用
this.key = key;
}
}
@Override
public void putObject(Object key, Object value) {
// 清除已經被 GC 的緩存項
removeGarbageCollectedItems();
// 添加緩存
delegate.putObject(key, new SoftEntry(key, value, queueOfGarbageCollectedEntries));
}
private void removeGarbageCollectedItems() {
SoftEntry sv;
// 周遊 queueOfGarbageCollectedEntries集合,清除已經被 GC 的緩存項 value
while ((sv = (SoftEntry) queueOfGarbageCollectedEntries.poll()) != null) {
delegate.removeObject(sv.key);
}
}
@Override
public Object getObject(Object key) {
Object result = null;
@SuppressWarnings("unchecked") // assumed delegate cache is totally managed by this cache
// 用一個軟引用指向 key 對應的緩存項
SoftReference<Object> softReference = (SoftReference<Object>) delegate.getObject(key);
// 檢測緩存中是否有對應的緩存項
if (softReference != null) {
// 擷取 softReference 引用的 value
result = softReference.get();
// 如果 softReference 引用的對象已經被 GC,則從緩存中清除對應的緩存項
if (result == null) {
delegate.removeObject(key);
} else {
synchronized (hardLinksToAvoidGarbageCollection) {
// 将緩存項的 value 添加到 hardLinksToAvoidGarbageCollection集合 中儲存
hardLinksToAvoidGarbageCollection.addFirst(result);
// 如果 hardLinksToAvoidGarbageCollection 的容積已經超過 numberOfHardLinks
// 則将最老的緩存項從 hardLinksToAvoidGarbageCollection 中清除,FIFO
if (hardLinksToAvoidGarbageCollection.size() > numberOfHardLinks) {
hardLinksToAvoidGarbageCollection.removeLast();
}
}
}
}
return result;
}
@Override
public Object removeObject(Object key) {
// 清除指定的緩存項之前,也會先清理被 GC 的緩存項
removeGarbageCollectedItems();
return delegate.removeObject(key);
}
@Override
public void clear() {
synchronized (hardLinksToAvoidGarbageCollection) {
// 清理強引用集合
hardLinksToAvoidGarbageCollection.clear();
}
// 清理被 GC 的緩存項
removeGarbageCollectedItems();
// 清理最底層的緩存項
delegate.clear();
}
@Override
public String getId() {
return delegate.getId();
}
@Override
public int getSize() {
removeGarbageCollectedItems();
return delegate.getSize();
}
public void setSize(int size) {
this.numberOfHardLinks = size;
}
}
WeakCache 的實作與 SoftCache 基本類似,唯一的差別在于其中使用 WeakEntry(繼承了 WeakReference)封裝真正的 value 對象,其他實作完全一樣。
另外,還有 ScheduledCache、LoggingCache、SynchronizedCache、SerializedCache 等。ScheduledCache 是周期性清理緩存的裝飾器,它的 clearInterval 字段 記錄了兩次緩存清理之間的時間間隔,預設是一小時,lastClear 字段 記錄了最近一次清理的時間戳。ScheduledCache 的 getObject()、putObject()、removeObject() 等核心方法,在執行時都會根據這兩個字段檢測是否需要進行清理操作,清理操作會清空緩存中所有緩存項。
LoggingCache 在 Cache 的基礎上提供了日志功能,它通過 hit 字段 和 request 字段 記錄了 Cache 的命中次數和通路次數。在 LoggingCache.getObject()方法 中,會統計命中次數和通路次數 這兩個名額,井按照指定的日志輸出方式輸出命中率。
SynchronizedCache 通過在每個方法上添加 synchronized 關鍵字,為 Cache 添加了同步功能,有點類似于 JDK 中 Collections 的 SynchronizedCollection 内部類。
SerializedCache 提供了将 value 對象 序列化的功能。SerializedCache 在添加緩存項時,會将 value 對應的 Java 對象 進行序列化,井将序列化後的 byte[]數組 作為 value 存入緩存 。SerializedCache 在擷取緩存項時,會将緩存項中的 byte[]數組 反序列化成 Java 對象。不使用 SerializedCache 裝飾器 進行裝飾的話,每次從緩存中擷取同一 key 對應的對象時,得到的都是同一對象,任意一個線程修改該對象都會影響到其他線程,以及緩存中的對象。而使用 SerializedCache 每次從緩存中擷取資料時,都會通過反序列化得到一個全新的對象。SerializedCache 使用的序列化方式是 Java 原生序列化。
2、CacheKey
在 Cache 中唯一确定一個緩存項,需要使用緩存項的 key 進行比較,MyBatis 中因為涉及 動态 SQL 等多方面因素, 其緩存項的 key 不能僅僅通過一個 String 表示,是以 MyBatis 提供了 CacheKey 類 來表示緩存項的 key,在一個 CacheKey 對象 中可以封裝多個影響緩存項的因素。CacheKey 中可以添加多個對象,由這些對象共同确定兩個 CacheKey 對象 是否相同。
public class CacheKey implements Cloneable, Serializable {
private static final long serialVersionUID = 1146682552656046210L;
public static final CacheKey NULL_CACHE_KEY = new NullCacheKey();
private static final int DEFAULT_MULTIPLYER = 37;
private static final int DEFAULT_HASHCODE = 17;
// 參與計算hashcode,預設值DEFAULT_MULTIPLYER = 37
private final int multiplier;
// 目前CacheKey對象的hashcode,預設值DEFAULT_HASHCODE = 17
private int hashcode;
// 校驗和
private long checksum;
private int count;
// 由該集合中的所有元素 共同決定兩個CacheKey對象是否相同,一般會使用一下四個元素
// MappedStatement的id、查詢結果集的範圍參數(RowBounds的offset和limit)
// SQL語句(其中可能包含占位符"?")、SQL語句中占位符的實際參數
private List<Object> updateList;
// 構造方法初始化屬性
public CacheKey() {
this.hashcode = DEFAULT_HASHCODE;
this.multiplier = DEFAULT_MULTIPLYER;
this.count = 0;
this.updateList = new ArrayList<>();
}
public CacheKey(Object[] objects) {
this();
updateAll(objects);
}
public void update(Object object) {
int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
// 重新計算count、checksum和hashcode的值
count++;
checksum += baseHashCode;
baseHashCode *= count;
hashcode = multiplier * hashcode + baseHashCode;
// 将object添加到updateList集合
updateList.add(object);
}
public int getUpdateCount() {
return updateList.size();
}
public void updateAll(Object[] objects) {
for (Object o : objects) {
update(o);
}
}
/**
* CacheKey重寫了 equals() 和 hashCode()方法,這兩個方法使用上面介紹
* 的 count、checksum、hashcode、updateList 比較兩個 CacheKey對象 是否相同
*/
@Override
public boolean equals(Object object) {
// 如果為同一對象,直接傳回 true
if (this == object) {
return true;
}
// 如果 object 都不是 CacheKey類型,直接傳回 false
if (!(object instanceof CacheKey)) {
return false;
}
// 類型轉換一下
final CacheKey cacheKey = (CacheKey) object;
// 依次比較 hashcode、checksum、count,如果不等,直接傳回 false
if (hashcode != cacheKey.hashcode) {
return false;
}
if (checksum != cacheKey.checksum) {
return false;
}
if (count != cacheKey.count) {
return false;
}
// 比較 updateList 中的元素是否相同,不同直接傳回 false
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;
}
@Override
public String toString() {
StringJoiner returnValue = new StringJoiner(":");
returnValue.add(String.valueOf(hashcode));
returnValue.add(String.valueOf(checksum));
updateList.stream().map(ArrayUtil::toString).forEach(returnValue::add);
return returnValue.toString();
}
@Override
public CacheKey clone() throws CloneNotSupportedException {
CacheKey clonedCacheKey = (CacheKey) super.clone();
clonedCacheKey.updateList = new ArrayList<>(updateList);
return clonedCacheKey;
}
}