一、Mybatis快取模塊分析
mybatis快取模塊具備以下特點:
- MyBatis 快取的實作是基于 Map 的,從快取里面讀寫資料是快取模塊的核心基礎功能;
- 除核心功能之外,有很多額外的附加功能,如:防止快取擊穿,添加快取清空策略(fifo、 lru)、序列化功能、日志能力、定時清空能力等;
- 附加功能可以以任意的組合附加到核心基礎功能之上;
那么我們應該如何優雅的為核心功能添加附加能力呢?
有些同學可能了解使用繼承的辦法擴展附加功能,繼承的方式是靜態的,用戶不能控制增加行為的方式和時機,另外,新功能的存在多種組合,使用繼承可能導致大量子類存在;
基于 Map 核心快取能力,將阻塞、清空策略、序列化、日志等等能力以任意組合的方式優 雅的增強是 Mybatis 快取模塊實作最大的難題,用動態代理或者繼承的方式擴展多種附加能力的傳統方式存在以下問題:這些方式是靜態的,用戶不能控制增加行為的方式和時機;另 外,新功能的存在多種組合,使用繼承可能導致大量子類存在,綜上,MyBtis 快取模塊采用 了裝飾器模式實作了快取模塊;
二、裝飾器模式
1、裝飾器模式介紹
裝飾器模式是一種用于代替繼承的技術,無需通過繼承增加子類就能擴展物件的新功能,使 用物件的關聯關系代替繼承關系,更加靈活,同時避免型別體系的快速膨脹,裝飾器 UML 類圖如下:

組件含義如下:
- 組件(Component):組件介面定義了全部組件類和裝飾器實作的行為;
- 組件實作類(ConcreteComponent):實作 Component 介面,組件實作類就是被裝飾器 裝飾的原始物件,新功能或者附加功能都是通過裝飾器添加到該類的物件上的
- 裝飾器抽象類(Decorator):實作 Component 介面的抽象類,在其中封裝了一個 Component 物件,也就是被裝飾的物件;
- 具體裝飾器類(ConcreteDecorator):該實作類要向被裝飾的物件添加某些功能;
我們很多人都玩過游戲,以DNF里的職業劍魂為例,裝飾器模式圖示如下: 
2、裝飾器模式優點
裝飾器相對于繼承,裝飾器模式靈活性更強,擴展性更強:
- 靈活性:裝飾器模式將功能切分成一個個獨立的裝飾器,在運行期可以根據需要動態的 添加功能,甚至對添加的新功能進行自由的組合;
- 擴展性:當有新功能要添加的時候,只需要添加新的裝飾器實作類,然后通過組合方式 添加這個新裝飾器,無需修改已有代碼,符合開閉原則;
3、裝飾器模式使用舉例
- IO 中輸入流和輸出流的設計 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream("c://a.txt")));
- 對網路爬蟲的自定義增強,可增強的功能包括:多執行緒能力、快取、自動生成報表、黑 白名單、random 觸發等
三、裝飾器在快取模塊的使用
MyBatis 快取模塊是一個經典的使用裝飾器實作的模塊,類圖如下:

- Cache:Cache 介面是快取模塊的核 心介面,定義了快取的基本操作;
- PerpetualCache:在快取模塊中扮演 ConcreteComponent 角色,使用 HashMap 來實作 cache 的相關操作;
- BlockingCache:阻塞版本的快取裝 飾器,保證只有一個執行緒到資料庫去查 找指定的 key 對應的資料;BlockingCache 是阻塞版本的快取裝飾器,這個裝飾器通過 ConcurrentHashMap 對鎖的粒度 進行了控制,提高加鎖后系統代碼運行的效率(注:快取雪崩的問題可以使用細粒度鎖的方 式提升鎖性能)
原始碼分析:
/**
* Simple blocking decorator
*
* Simple and inefficient version of EhCache's BlockingCache decorator.
* 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對應的資料
*
*
*/
public class BlockingCache implements Cache {
//阻塞的超時時長
private long timeout;
//被裝飾的底層物件,一般是PerpetualCache
private final Cache delegate;
//鎖物件集,粒度到key值
private final ConcurrentHashMap<Object, ReentrantLock> locks;
public BlockingCache(Cache delegate) {
this.delegate = delegate;
this.locks = new ConcurrentHashMap<>();
}
@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 {
releaseLock(key);
}
}
@Override
public Object getObject(Object key) {
acquireLock(key);//根據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;
}
@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);//把新鎖添加到locks集合中,如果添加成功使用新鎖,如果添加失敗則使用locks集合中的鎖
return previous == null ? lock : previous;
}
//根據key獲得鎖物件,獲取鎖成功加鎖,獲取鎖失敗阻塞一段時間重試
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("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 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;
}
}
除了 BlockingCache 之外,快取模塊還有其他的裝飾器如:
- LoggingCache:日志能力的快取;
- ScheduledCache:定時清空的快取;
- BlockingCache:阻塞式快取;
- SerializedCache:序列化能力的快取;
- SynchronizedCache:進行同步控制的快取;
那么問題來了,我們知道HashMap是執行緒不安全的,那么Mybatis 的快取功能使用 HashMap 實作會不會出現并發安全的問題呢?
MyBatis 的快取分為一級快取、二級快取,二級快取是多個會話共享的快取,確實會出 現并發安全的問題,因此 MyBatis 在初始化二級快取時,會給二級快取默認加上 SynchronizedCache 裝飾器的增強,在對共享資料 HashMap 操作時進行同步控制,所以二級 快取不會出現并發安全問題;而一級快取是會話獨享的,不會出現多個執行緒同時操作快取數 據的場景,因此一級快取也不會出現并發安全的問題;
四、快取的唯一標識 CacheKey
MyBatis 中涉及到動態 SQL 的原因,快取項的 key 不能僅僅通過一個 String 來表示,所以通 過 CacheKey 來封裝快取的 Key 值,CacheKey 可以封裝多個影響快取項的因素;
判斷兩個 CacheKey是否相同關鍵是比較兩個物件的hash值是否一致;構成CacheKey物件的要素包括:
- mappedStatment 的 id
- 指定查詢結果集的范圍(分頁資訊)
- 查詢所使用的 SQL 陳述句
- 用戶傳遞給 SQL 陳述句的實際引數值
CacheKey 中 update 方法和 equals 方法是進行比較時非常重要的兩個方法:
- update 方法:用于添加構成 CacheKey 物件的要素,每添加一個元素會對 hashcode、checksum、count 以及 updateList 進行更新;
- equals 方法:用于比較兩個元素是否相等,首先比較 hashcode、checksum、count 是否 相等,如果這三個值相等,會回圈比較 updateList 中每個元素的 hashCode 是否一致;
按照這種方式判斷兩個物件是否相等,一方面能很嚴格的判斷是否一致避免出現誤判, 另外一方面能提高比較的效率;
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/321210.html
標籤:java
