在 Web 應用中,緩存是必不可少的組件。一般咱們都會用 Redis 或 memcached 等緩存中間件,攔截大量奔向數據庫的請求,減輕數據庫壓力。做爲一個重要的組件,MyBatis 天然也在內部提供了相應的支持。經過在框架層面增長緩存功能,可減輕數據庫的壓力,同時又能夠提高查詢速度,可謂一箭雙鵰。MyBatis 緩存結構由一級緩存和二級緩存構成,這兩級緩存均是使用 Cache 接口的實現類。所以,在接下里的章節中,我將首先會向你們介紹 Cache 幾種實現類的源碼,而後再分析一級和二級緩存的實現。算法
本文主要內容:數據庫
Mybatis跟緩存相關的類都在cache包目錄下,在前面的文章中咱們也提過,今天才來詳細說說。其中有一個頂層接口Cache,而且只有一個默認的實現類PerpetualCache。設計模式
下面是Cached的類圖:緩存
既然PerpetualCache是默認實現類,那麼咱們就從他下手。框架
PerpetualCache這個對象會建立,因此這個叫作基礎緩存。可是緩存又能夠有不少額外的功能,好比說:回收策略、日誌記錄、定時刷新等等,若是須要的話,就能夠在基礎緩存上加上這些功能,若是不喜歡就不加。這裏是否是想到了一種設計模式-----裝飾器設計模式。PerpetualCache 至關於裝飾模式中的 ConcreteComponent。ide
裝飾器模式是指在不改變原有對象的基礎之上,將功能附加到對象上,提供了比繼承更有彈性的替換方案,即擴展原有對象的功能。memcached
除了緩存以外,Mybatis也定義不少的裝飾器,一樣實現了Cache接口,經過這些裝飾器能夠額外實現不少功能。ui
這些緩存是怎麼分類的呢?this
全部的緩存能夠大致歸爲三類:基本類緩存、淘汰算法緩存、裝飾器緩存。url
下面把每一個緩存進行詳細說明和對比:
緩存實現類源碼
PerpetualCache源碼
PerpetualCache 是一個具備基本功能的緩存類,內部使用了 HashMap 實現緩存功能。它的源碼以下:
public class PerpetualCache implements 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();
}
// 存儲鍵值對到 HashMap
@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();
}
//部分代碼省略
}
上面是 PerpetualCache 的所有代碼,也就是所謂的基本緩存,很簡單。接下來,咱們經過裝飾類對該類進行裝飾,使其功能變的豐富起來。
LruCache,顧名思義,是一種具備 LRU(Least recently used,最近最少使用)算法的緩存實現類。
除此以外,MyBatis 還提供了具備 FIFO 策略的緩存 FifoCache。不過並未提供 LFU (Least Frequently Used ,最近最少使用算法)緩存,也是一種常見的緩存算法 ,若是你們有興趣,能夠自行拓展。
接下來,咱們來看一下 LruCache 的實現。
public class LruCache implements Cache {
private final Cache delegate;
private Map<Object, Object> keyMap;
private Object eldestKey;
public LruCache(Cache delegate) {
this.delegate = delegate;
setSize(1024);
}
public int getSize() {
return delegate.getSize();
}
public void setSize(final int size) {
/*
* 初始化 keyMap,注意,keyMap 的類型繼承自 LinkedHashMap,
* 並覆蓋了 removeEldestEntry 方法
*/
keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
private static final long serialVersionUID = 4267176411845948333L;
// 覆蓋 LinkedHashMap 的 removeEldestEntry 方法
@Override
protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
boolean tooBig = size() > size;
if (tooBig) {
// 獲取將要被移除緩存項的鍵值
eldestKey = eldest.getKey();
}
return tooBig;
}
};
}
@Override
public void putObject(Object key, Object value) {
// 存儲緩存項
delegate.putObject(key, value);
cycleKeyList(key);
}
@Override
public Object getObject(Object key) {
// 刷新 key 在 keyMap 中的位置
keyMap.get(key);
// 從被裝飾類中獲取相應緩存項
return delegate.getObject(key);
}
@Override
public Object removeObject(Object key) {
// 從被裝飾類中移除相應的緩存項
return delegate.removeObject(key);
}
//清空緩存
@Override
public void clear() {
delegate.clear();
keyMap.clear();
}
private void cycleKeyList(Object key) {
// 存儲 key 到 keyMap 中
keyMap.put(key, key);
if (eldestKey != null) {
// 從被裝飾類中移除相應的緩存項
delegate.removeObject(eldestKey);
eldestKey = null;
}
}
// 省略部分代碼
}
從上面代碼中能夠看出,LruCache 的 keyMap 屬性是實現 LRU 策略的關鍵,該屬性類型繼承自 LinkedHashMap,並覆蓋了 removeEldestEntry 方法。LinkedHashMap 可保持鍵值對的插入順序,當插入一個新的鍵值對時,
LinkedHashMap 內部的 tail 節點會指向最新插入的節點。head 節點則指向第一個被插入的鍵值對,也就是最久未被訪問的那個鍵值對。默認狀況下,LinkedHashMap 僅維護鍵值對的插入順序。若要基於 LinkedHashMap 實現 LRU 緩存,還需經過構造方法將 LinkedHashMap 的 accessOrder 屬性設爲 true,此時 LinkedHashMap 會維護鍵值對的訪問順序。
好比,上面代碼中 getObject 方法中執行了這樣一句代碼 keyMap.get(key),目的是刷新 key 對應的鍵值對在 LinkedHashMap 的位置。LinkedHashMap 會將 key 對應的鍵值對移動到鏈表的尾部,尾部節點表示最久剛被訪問過或者插入的節點。除了需將 accessOrder 設爲 true,還需覆蓋 removeEldestEntry 方法。LinkedHashMap 在插入新的鍵值對時會調用該方法,以決定是否在插入新的鍵值對後,移除老的鍵值對。
在上面的代碼中,當被裝飾類的容量超出了 keyMap 的所規定的容量(由構造方法傳入)後,keyMap 會移除最長時間未被訪問的鍵,並保存到 eldestKey 中,而後由 cycleKeyList 方法將 eldestKey 傳給被裝飾類的 removeObject 方法,移除相應的緩存項目。
BlockingCache 實現了阻塞特性,該特性是基於 Java 重入鎖實現的。同一時刻下,BlockingCache 僅容許一個線程訪問指定 key 的緩存項,其餘線程將會被阻塞住。
下面咱們來看一下 BlockingCache 的源碼。
public class BlockingCache implements Cache {
private long timeout;
private final Cache delegate;
private final ConcurrentHashMap<Object, ReentrantLock> locks;
public BlockingCache(Cache delegate) {
this.delegate = delegate;
this.locks = new ConcurrentHashMap<Object, ReentrantLock>();
}
@Override
public void putObject(Object key, Object value) {
try {
// 存儲緩存項
delegate.putObject(key, value);
} finally {
// 釋放鎖
releaseLock(key);
}
}
@Override
public Object getObject(Object key) {
// 請 // 請求鎖
acquireLock(key);
Object value = delegate.getObject(key);
// 若緩存命中,則釋放鎖。須要注意的是,未命中則不釋放鎖
if (value != null) {
// 釋放鎖
releaseLock(key);
}
return value;
}
@Override
public Object removeObject(Object key) {
// 釋放鎖
releaseLock(key);
return null;
}
private ReentrantLock getLockForKey(Object key) {
ReentrantLock lock = new ReentrantLock();
// 存儲 <key, Lock> 鍵值對到 locks 中
ReentrantLock previous = locks.putIfAbsent(key, lock);
return previous == null ? lock : previous;
}
private void acquireLock(Object key) {
Lock lock = getLockForKey(key);
if (timeout > 0) {
try {
// 嘗試加鎖
boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS);
if (!acquired) {
throw new CacheException("...");
}
} catch (InterruptedException e) {
throw new CacheException("...");
}
} else {
// 加鎖
lock.lock();
}
}
private void releaseLock(Object key) {
// 獲取與當前 key 對應的鎖
ReentrantLock lock = locks.get(key);
if (lock.isHeldByCurrentThread()) {
// 釋放鎖
lock.unlock();
}
}
// 省略部分代碼
}
如上,查詢緩存時,getObject 方法會先獲取與 key 對應的鎖,並加鎖。若緩存命中,getObject 方法會釋放鎖,不然將一直鎖定。getObject 方法若返回 null,表示緩存未命中。此時 MyBatis 會進行數據庫查詢,並調用 putObject 方法存儲查詢結果。同時,putObject 方法會將指定 key 對應的鎖進行解鎖,這樣被阻塞的線程便可恢復運行。
上面的描述有點囉嗦,卻是 BlockingCache 類的註釋說到比較簡單明瞭。這裏引用一下:
It sets a lock over a cache key when the element is not found in cache.
This way, other threads will wait until this element is filled instead of hitting the database.
這段話的意思是,當指定 key 對應元素不存在於緩存中時,BlockingCache 會根據 lock 進行加鎖。此時,其餘線程將會進入等待狀態,直到與 key 對應的元素被填充到緩存中。而不是讓全部線程都去訪問數據庫。
在上面代碼中,removeObject 方法的邏輯很奇怪,僅調用了 releaseLock 方法釋放鎖,卻沒有調用被裝飾類的 removeObject 方法移除指定緩存項。這樣作是爲何呢?你們能夠先思考,答案將在分析二級緩存的相關邏輯時分析。
在 MyBatis 中,引入緩存的目的是爲提升查詢效率,下降數據庫壓力。既然 MyBatis 引入了緩存,那麼你們思考過緩存中的 key 和 value 的值分別是什麼嗎?你們可能很容易能回答出 value 的內容,不就是 SQL 的查詢結果嗎。
那 key 是什麼呢?是字符串,仍是其餘什麼對象?若是是字符串的話,那麼你們首先能想到的是用 SQL 語句做爲 key。但這是不對的.
好比:
SELECT * FROM author where id > ?
d > 1 和 id > 10 查出來的結果多是不一樣的,因此咱們不能簡單的使用 SQL 語句做爲 key。從這裏能夠看出來,運行時參數將會影響查詢結果,所以咱們的 key 應該涵蓋運行時參數。除此以外呢,若是進行分頁查詢也會致使查詢結果不一樣,所以 key 也應該涵蓋分頁參數。綜上,咱們不能使用簡單的 SQL 語句做爲 key。應該考慮使用一種複合對象,能涵蓋可影響查詢結果的因子。在 MyBatis 中,這種複合對象就是 CacheKey。
下面來看一下它的定義。
public class CacheKey implements Cloneable, Serializable {
private static final int DEFAULT_MULTIPLYER = 37;
private static final int DEFAULT_HASHCODE = 17;
// 乘子,默認爲37
private final int multiplier;
// CacheKey 的 hashCode,綜合了各類影響因子
private int hashcode;
// 校驗和
private long checksum;
// 影響因子個數
private int count;
// 影響因子集合
private List<Object> updateList;
public CacheKey() {
this.hashcode = DEFAULT_HASHCODE;
this.multiplier = DEFAULT_MULTIPLYER;
this.count = 0;
this.updateList = new ArrayList<Object>();
}
// 省略其餘方法
}
如上,除了 multiplier 是恆定不變的 ,其餘變量將在更新操做中被修改。
下面看一下更新操做的代碼。
/** 每當執行更新操做時,表示有新的影響因子參與計算 */
public void update(Object object) {
int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
// 自增 count
count++;
// 計算校驗和
checksum += baseHashCode;
// 更新 baseHashCode
baseHashCode *= count;
// 計算 hashCode
hashcode = multiplier * hashcode + baseHashCode;
// 保存影響因子
updateList.add(object);
}
當不斷有新的影響因子參與計算時,hashcode 和 checksum 將會變得愈發複雜和隨機。這樣可下降衝突率,使 CacheKey 可在緩存中更均勻的分佈。CacheKey 最終要做爲鍵存入 HashMap,所以它須要覆蓋 equals 和 hashCode 方法。
下面咱們來看一下這兩個方法的實現。
public boolean equals(Object object) {
// 檢測是否爲同一個對象
if (this == object) {
return true;
}
// 檢測 object 是否爲 CacheKey
if (!(object instanceof CacheKey)) {
return false;
}
final CacheKey cacheKey = (CacheKey) object;
// 檢測 hashCode 是否相等
if (hashcode != cacheKey.hashcode) {
return false;
}
// 檢測校驗和是否相同
if (checksum != cacheKey.checksum) {
return false;
}
// 檢測 coutn 是否相同
if (count != cacheKey.count) {
return 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;
}
public int hashCode() {
// 返回 hashcode 變量
return hashcode;
}
equals 方法的檢測邏輯比較嚴格,對 CacheKey 中多個成員變量進行了檢測,已保證二者相等。hashCode 方法比較簡單,返回 hashcode 變量便可。
關於 CacheKey 就先分析到這,CacheKey 在一二級緩存中會被用到,接下來還會看到它的身影。
好吧,終於把源碼緩存實現類的源碼說完了。