寫在前面
聊一聊MyBatis是如何使用裝飾者模式的,順便回顧下緩存的相關知識,可以看看右側目錄一覽內容概述。
裝飾者模式
這里就不聊它的概念了,總結下就是套娃。利用組合的方式將裝飾器組合進來,增強共同的抽象方法(與代理很類似但是又更靈活)
MyBatis緩存
回憶下傳統手藝
<!-- 先進先出,60秒刷新一次,可存儲512個引用,返回對象只讀,不同線程中的調用者之間修改會導致沖突 -->
<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
粗略回顧下MyBatis緩存
一級緩存
MyBatis的一級緩存存在於SqlSession的生命周期中,在同一個SqlSession中查詢時,MyBatis會把執行的方法和參數通過算法生成緩存的鍵值,將鍵值和查詢結果存入一個Map對象中。如果同一個SqlSession中執行的方法和參數完全一致,那么通過算法會生成相同鍵值,當Map緩存對象中已經存在該鍵值時,則會返回緩存中的對象。
默認開啟
二級緩存
MyBatis的二級緩存非常強大,它不同於一級緩存只存在於SqlSession的生命周期中,而是可以理解為存在於SqlSessionFactory的生命周期中。
默認不開啟,需要如下配置后開啟全局配置,再在對應的Mapper.xml中添加“傳統手藝”-
<settings>
<setting name = "cacheEnabled" value="true"/>
</settings>
<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
另一種開啟方式-注解
@CacheNamespace(
eviction = FifoCache.class,
flushInterval = 60000,
size = 512,
readWrite = true
)
public interface RoleMapper {
// 接口方法
}
- eviction(收回策略)
- LRU(最近最少使用的):移除長時間不使用的對象,這是默認值
- FIFO(先進先出):按對象進入緩存的順序來移除它們
- SOFT(軟引用):移除基於垃圾回收器狀態和軟引用規則的對象
- WEAK(弱引用):更積極地移除基於垃圾收集器狀態和弱引用規則的對象
- flushInterval(刷新間隔)
- size(引用數目)
- readOnly(只讀)只讀的緩存會給所有調用者返回緩存的相同實例,因此這些對象不能被修改,這提供了很重要的性能優勢。可讀寫的緩存會通過序列化返回緩存對象的拷貝,這種方式會慢一些,但是安全,因此默認是false
集成第三方緩存
MyBatis還支持通過“type”來集成第三方緩存,如下就是集成了Redis緩存,這樣就從本地緩存跳躍到了分布式緩存了。
<mapper namespace="xxx.xxx.xxx.mapper.RoleMapper">
<!-- 集成Redis緩存-->
<cache type="org.mybatis.caches.redis.RedisCache" />
</mapper>
二級緩存的問題-臟數據
二級緩存雖然能提高應用效率,減輕數據庫服務器的壓力,但是如果使用不當,很容易產生臟數據
MyBatis的二級緩存是和命名空間綁定的,所以通常情況下每一個Mapper映射文件都擁有自己的二級緩存,不同Mapper的二級緩存互不影響。在常見的數據庫操作中,多表聯合查詢非常常見,由於關系型數據庫的設計,使得很多時候需要關聯多個表才能獲得想要的數據。在關聯多表查詢時肯定會將查詢放到某個命名空間下的映射文件中,這樣一個多表的查詢就會緩存在該命名空間的二級緩存中。涉及這些表的增刪改操作通常不在一個映射文件中,它們的命名空間不同,因此當有數據變化時,多表查詢的緩存未必會被清空,這種情況下就會產生臟數據。
基於MyBatis緩存機制結合源碼解析裝飾器模式
Cache接口:
Cache核心方法:
- putObject
- getObject
- removeObject
DEMO-實戰使用MyBatis的裝飾者模式
public static void main(String[] args) {
final String cacheKey = "cache";
final Cache cache = new LoggingCache(new BlockingCache(new PerpetualCache(cacheKey)));
Object cacheValue = cache.getObject(cacheKey);
if (Objects.isNull(cacheValue)) {
log.debug("緩存未命中 >>>>>>>>> key:[{}]", cacheKey);
cache.putObject(cacheKey, "MyCacheValue");
}
cacheValue = cache.getObject(cacheKey);
log.debug("緩存命中 >>>>>>>>> key:[{}],value:[{}]", cacheKey, cacheValue);
}
如代碼所示,是不是看到了“裝飾者模式”的影子了,在構造函數中瘋狂套娃。使用的是MyBatis的API,給基本緩存組件裝飾了“日志打印”、“阻塞“的能力。
結果演示:
可以看到,LogginCache在讀緩存的時候還會打印出緩存命中率。 好了,接下來進入正題,看看其他緩存是怎么實現的吧。以下源碼基於MyBatis3.4.5
PerpetualCache
private final Map<Object, Object> cache = new 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);
}
這是MyBatis的基礎緩存,套娃的基本得有它,它的核心就是個HashMap來作為緩存容器,其實現的Cache接口的幾個核心方法也都是委托給了HashMap去做。
FifoCache
一個支持先進先出的緩存策略的MyBatisCache
private final Cache delegate;
//維護一個key的雙端隊列
private final Deque<Object> keyList;
private int size;
public FifoCache(Cache delegate) {
//通過構造函數,將Cache組合進來,取名”委托“
this.delegate = delegate;
this.keyList = new LinkedList<>();
this.size = 1024;
}
@Override
public void putObject(Object key, Object value) {
//先走自己的增強
cycleKeyList(key);
//真實的寫緩存交給”委托“去做
delegate.putObject(key, value);
}
@Override
public Object getObject(Object key) {
return delegate.getObject(key);
}
@Override
public Object removeObject(Object key) {
return delegate.removeObject(key);
}
private void cycleKeyList(Object key) {
//將新寫的緩存key添加到雙端隊列末尾
keyList.addLast(key);
// 如果key的大小大於了1024(構造函數中默認賦值1024)則會移除最早添加的緩存
// 1. 移除自身維護的key隊列的隊頭 2.委托給“委托”去真實刪除隊頭緩存對象
if (keyList.size() > size) {
Object oldestKey = keyList.removeFirst();
delegate.removeObject(oldestKey);
}
}
以上就是MyBatis先進先出緩存的實現了,FifoCache維護了key的雙端隊列,每次寫緩存的時候會判斷大小如果大於閾值則會先移除隊頭的key,再委托給組合進來的Cache來刪除對應緩存操作,完成“先進先出”的增強(裝飾)
LruCache
一個支持LRU(Least Recently Used ,最近最少使用)緩存策略的MyBatisCache
回憶下緩存策略
- LRU:Least Recently Used,最近最少使用
- LFU:Least Frequently Used,最近不常被使用
LRU 算法有一個缺點,比如說很久沒有使用的一個鍵值,如果最近被訪問了一次,那么即使它是使用次數最少的緩存,它也不會被淘汰;而 LFU 算法解決了偶爾被訪問一次之后,數據就不會被淘汰的問題,它是根據總訪問次數來淘汰數據的,其核心思想是“如果數據過去被訪問多次,那么將來它被訪問次數也會比較多”。因此 LFU 可以理解為比 LRU 更加合理的淘汰算法。
回憶下LinkedHashMap的核心機制-LRU
LinkedHashMap相比HashMap多了兩個節點,before,after這樣就能夠維護節點之間的順序了。
我們看看LinkedHashMap的get方法,它內部有LinkedHashMap開啟LRU機制的秘密。
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder) // 為true則會執行afterNodeAccess(將節點移動到隊尾)
afterNodeAccess(e);
return e.value;
}
void afterNodeAccess(Node<K,V> e) { // move node to last (官方注釋 言簡意賅 -> 將節點移動到隊尾)
LinkedHashMap.Entry<K,V> last;
if (accessOrder && (last = tail) != e) {
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
那么這個accessOrder變量是怎么維護的呢?看代碼
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
你會發現,LinkedHashMap有這么一個構造函數,第三個參數便是accessOrder,所以決定是否開啟LRU是你在運行時傳參決定的!開啟后則會在每次讀取鍵值對之后將讀取的節點移動至隊尾,那么隊頭就是最近最少使用的了,隊尾就是剛剛使用的了,當需要刪除最近最少使用的節點的時候,直接刪除隊頭的即可。
回憶下LinkedHashMap的核心方法-removeEldestEntry
LinkedHashMap是一個有順序的HashMap,它可以使得你的k,v能夠按照某種順序寫入和讀取,它的核心方法removeEldestEntry功不可沒。
在HashMap新增k,v之后會回調一個方法“afterNodeInsertion”,這個方法在HashMap中是一個空實現(俗稱鈎子方法),它的子類LinkedHashMap重寫了它,代碼如下。
void afterNodeInsertion(boolean evict) { // possibly remove eldest 這是官方注釋,言簡意賅(可能會刪除老key)
LinkedHashMap.Entry<K,V> first;
//前面的短路方法不管,我們關注removeEldestEntry方法 -> 如果該方法也返回true,則會走方法體中的removeNode方法(刪除first節點的元素)。
// 當開啟LinkedHashMap的LRU模式,則隊頭的元素是“最近最少使用的元素”,因為每次讀取k,v后都會將元素調整至隊尾,所以隊頭的元素是“最近最少使用的元素“
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
進入正題
private final Cache delegate;
// 維護一個key和value都是緩存key的map
private Map<Object, Object> keyMap;
//最近最少使用的Key
private Object eldestKey;
public LruCache(Cache delegate) {
//通過構造函數,將Cache組合進來,取名”委托“
this.delegate = delegate;
//初始化keyMap(重要)
setSize(1024);
}
public void setSize(final int size) {
// 構造函數第三個參數傳遞true(accessOrder),如上所述將開啟LRU模式
keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
private static final long serialVersionUID = 4267176411845948333L;
// 重寫了LinkedHashMap的方法
@Override
protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
boolean tooBig = size() > size;
if (tooBig) {
// 大小超過閾值,將隊頭(最近最少使用)的key更新至自身維護的"eldestKey" (重要)
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) {
keyMap.get(key); // touch
return delegate.getObject(key);
}
@Override
public Object removeObject(Object key) {
return delegate.removeObject(key);
}
private void cycleKeyList(Object key) {
// 因為重寫了LinkedHashMap的removeEldestEntry方法,如上所述,超過閾值后eldestKey指向的就是最近最少使用的key
keyMap.put(key, key);
if (eldestKey != null) {
// 委托移除最近最少使用的緩存
delegate.removeObject(eldestKey);
// 置空
eldestKey = null;
}
}
以上就是MyBatis中的LRU緩存的機制了,自身維護了一個LinkedHashMap,開啟了LRU機制,重寫了removeEldestEntry方法,當大小觸發閾值的時候維護最近最少使用的元素key,委托給組合進來的Cache對象移除,整個流程下來就使得被裝飾着有了LRU的增強。
SoftCache
一個軟引用的MyBatisCache
弱引用
弱引用比強引用稍弱一些。當JVM內存不足時,GC才會回收那些只被軟引用指向的對象,從而避免OutOfMemoryError。當GC將只被軟引用指向的對象全部回收之后,內存依然不足時,JVM才會拋出OutOfMemoryError。(這一特性非常適合做緩存,畢竟最終數據源在DB,還能保護JVM進程)
// 維護最近經常使用的緩存數據,該集合會使用強引用指向其中的每個緩存Value,防止被GC回收
private final Deque<Object> hardLinksToAvoidGarbageCollection;
//與SortEntry對象關聯,用於記錄已經被回收的緩存條目
private final ReferenceQueue<Object> queueOfGarbageCollectedEntries;
private final Cache delegate;
//強引用的個數,默認256。即有256個熱點數據無法直接被GC回收
private int numberOfHardLinks;
public SoftCache(Cache delegate) {
this.delegate = delegate;
this.numberOfHardLinks = 256;
this.hardLinksToAvoidGarbageCollection = new LinkedList<Object>();
this.queueOfGarbageCollectedEntries = new ReferenceQueue<Object>();
}
@Override
public void putObject(Object key, Object value) {
// 同步刪除已經被GC回收的Value
removeGarbageCollectedItems();
delegate.putObject(key, new SoftEntry(key, value, queueOfGarbageCollectedEntries));
}
private static class SoftEntry extends SoftReference<Object> {
private final Object key;
SoftEntry(Object key, Object value, ReferenceQueue<Object> garbageCollectionQueue) {
// 關聯引用隊列。
// 當SoftReference指向的對象被回收的時候,JVM就會將這個SoftReference作為通知,添加到與其關聯的引用隊列
super(value, garbageCollectionQueue);
this.key = key;
}
}
@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) {
// 重要的一步!判斷Value是否為空,為空則表示弱引用指向的對象已經被GC回收了,就需要同步刪除該緩存。
delegate.removeObject(key);
} else {
// See #586 (and #335) modifications need more than a read lock
// 讀取緩存后,維護“強引用”的數據。
synchronized (hardLinksToAvoidGarbageCollection) {
hardLinksToAvoidGarbageCollection.addFirst(result); // 將緩存添加進強引用隊列(熱點數據)
if (hardLinksToAvoidGarbageCollection.size() > numberOfHardLinks) {
hardLinksToAvoidGarbageCollection.removeLast(); // 維護隊列個數
}
}
}
}
return result;
}
@Override
public Object removeObject(Object key) {
removeGarbageCollectedItems(); // 刪除被GC回收的Value
return delegate.removeObject(key); // 委托刪除緩存
}
private void removeGarbageCollectedItems() {
SoftEntry sv;
// 引用關聯的隊列如果有值,則說明有被GC回收的Value
while ((sv = (SoftEntry) queueOfGarbageCollectedEntries.poll()) != null) {
delegate.removeObject(sv.key);
}
}
WeakCache
一個弱引用的MyBatisCache
與弱引用類似(基本相同),不過多介紹了。
弱引用
弱引用比軟引用的引用強度還要弱。弱引用可以引用一個對象,但無法阻止這個對象被GC回收,也就是說,在JVM進行垃圾回收的時候,若發現某個對象只有一個弱引用指向它,那么這個對象會被GC立刻回收。(即遇GC比死,存活的時間為兩次GC之間)
// Entry繼承的是WeakReference。
// 其他內容參考弱引用Cache
private static class WeakEntry extends WeakReference<Object> {
private final Object key;
private WeakEntry(Object key, Object value, ReferenceQueue<Object> garbageCollectionQueue) {
super(value, garbageCollectionQueue);
this.key = key;
}
}
LoggingCache
一個支持打印Debug級別的緩存命中率的MyBatisCache
// 日志打印的log對象
private final Log log;
private final Cache delegate;
// 請求數
protected int requests = 0;
// 緩存命中數
protected int hits = 0;
public LoggingCache(Cache delegate) {
//通過構造函數,將Cache組合進來,取名”委托“
this.delegate = delegate;
//log通過緩存id作為表示
this.log = LogFactory.getLog(getId());
}
@Override
public void putObject(Object key, Object object) {
delegate.putObject(key, object);
}
@Override
public Object getObject(Object key) {
requests++; // 請求數增加
final Object value = delegate.getObject(key);
if (value != null) {
hits++; // 緩存命中,命中數增加
}
if (log.isDebugEnabled()) {
log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio()); // 打印緩存命中率
}
return value;
}
@Override
public Object removeObject(Object key) {
return delegate.removeObject(key);
}
private double getHitRatio() {
// 計算緩存命中率
return (double) hits / (double) requests;
}
LoggingCache使得緩存讀取的時候能夠有緩存命中率的日志打印,挺實用的增強。
BlockingCache
一個支持阻塞的MyBatisCache
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<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) {
// despite of its name, this method is called only to release locks
releaseLock(key); // 釋放鎖
return null;
}
private void acquireLock(Object key) {
Lock lock = getLockForKey(key); // 獲取對應的Lock,沒有則新增一把Lock
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) {
ReentrantLock lock = new ReentrantLock();
ReentrantLock previous = locks.putIfAbsent(key, lock);
return previous == null ? lock : previous;
}
private void releaseLock(Object key) {
ReentrantLock lock = locks.get(key); // 獲取Key對應的Lock
if (lock.isHeldByCurrentThread()) { // 如果是當前線程持有lock,則釋放鎖
lock.unlock();
}
}
SynchronizedCache
一個支持同步的MyBatisCache,從名稱就能知道實現原理是synchronized關鍵字
public SynchronizedCache(Cache delegate) {
this.delegate = delegate;
}
@Override
public synchronized int getSize() {
return delegate.getSize();
}
@Override
public synchronized void putObject(Object key, Object object) {
delegate.putObject(key, object);
}
@Override
public synchronized Object getObject(Object key) {
return delegate.getObject(key);
}
@Override
public synchronized Object removeObject(Object key) {
return delegate.removeObject(key);
}
同步緩存就是給核心方法加上了同步鎖,保證了線程安全。
跟隨源碼看看解析-裝飾過程
cacheElement方法解析cache標簽
可以看出最底層是PerpetualCache,默認裝飾的是LruCache。
如下就是將剩下的裝飾器循環裝飾的過程了,細節就不追進去了。
以上就是MyBatis對於緩存的裝飾者設計模式的實踐相關的源碼簡單追蹤了。
跟隨源碼看看緩存的使用的地方
先隨便點擊Cache接口的一方法,看看在哪里有使用。很明顯,那個BaseExecutor的類就是正兒八經使用的地方。
query方法中很明顯表示了先從緩存中獲取,如果沒有則走DB(還會寫緩存)
代碼也很簡單,就是從DB獲取然后寫入緩存
總結
筆者先簡單描述了裝飾者模式,隨后回憶了MyBatis的緩存傳統手藝-cache標簽的使用,以及一級二級緩存,描述了集成第三方緩存(解決JVM緩存的單點問題)。
隨后結合源碼介紹了MyBatis的Cache接口及其相關的實現類,首先通過Demo言簡意賅地表達了裝飾者模式的使用以及MyBatisCache裝飾者模式使用的效果(LoggingCache)
緊接着筆者介紹了
- PerpetualCache這個最關鍵最核心的緩存實現類,它的核心是一個HashMap;
- FifoCache先進先出淘汰策略的緩存實現類,它的核心是一個維護key的雙端隊列,添加緩存前先維護這個雙端隊列,如果size到達閾值則移除隊頭的元素;
- LruCache最近最少使用淘汰策略的緩存實現類,它的核心是基於LinkedHashMap實現LRU機制,我們也回憶了LRU以及LinkedHashMap相關的知識點,其關鍵點就是一個繼承了LinkedHashMap的keyMap(KV都是緩存Key),重寫了LinkedHashMap的重要方法removeEldestEntry,用於記錄最近最少使用的key,在適當時機刪除該緩存;
- SoftCache、WeakCache我們回憶了軟引用、弱引用的相關知識,其核心就是對應的Value組件Entry繼承了SoftReference、WeakReference;
- BlockingCache這個阻塞緩存的核心就是大名鼎鼎的ReentrantLock;
- SynchronizedCache這個緩存顧名思義就是核心方法追加了synchronized的關鍵字,事實也確實如此。
為什么要使用緩存?走DB的鏈路上層用緩存抗一抗再正常不過了。 為什么用裝飾者模式?這個場景它的核心就是緩存策略有很多,它們互相可以疊加,可以在配置的時候靈活配置,那么就可以通過解析配置后在運行時靈活的“裝飾”起來,達到最后的預期效果,挺妙的。
關於多種Cache的核心實現,以及相關的周邊技術可以反復琢磨,比如鎖的使用、緩存的讀寫、LinkedHashMap、JVM的GC等等,畢竟這是開源框架的實戰代碼,這些都是值得我們像駱駝一樣反復咀嚼,反復反芻的,至少了解了這一塊,后續你真的有類似實戰的時候之前可以先參考參考了!
好了,以上就是MyBatis緩存解析-裝飾者設計模式了。歡迎多多交流,希望對你有幫助。原創不易..(沒想到這么難,本來想總結下,發現一兩次還寫不完,光扣字都扣傻了 哈哈..)