Myabtis原始碼分析四-快取模組分析 ,裝飾模式的使用

語言: CN / TW / HK

​一起養成寫作習慣!這是我參與「掘金日新計劃 · 4 月更文挑戰」的第9天,[點選檢視活動詳情]

一、Mybatis快取模組分析

mybatis快取模組具備以下特點:

  1. MyBatis 快取的實現是基於 Map 的,從快取裡面讀寫資料是快取模組的核心基礎功能;
  2. 除核心功能之外,有很多額外的附加功能,如:防止快取擊穿,新增快取清空策略(fifo、 lru)、序列化功能、日誌能力、定時清空能力等;
  3. 附加功能可以以任意的組合附加到核心基礎功能之上;

那麼我們應該如何優雅的為核心功能新增附加能力呢?

有些同學可能瞭解使用繼承的辦法擴充套件附加功能,繼承的方式是靜態的,使用者不能控制增加行為的方式和時機。另外,新功能的存在多種組合,使用繼承可能導致大量子類存在;

基於 Map 核心快取能力,將阻塞、清空策略、序列化、日誌等等能力以任意組合的方式優 雅的增強是 Mybatis 快取模組實現最大的難題,用動態代理或者繼承的方式擴充套件多種附加能力的傳統方式存在以下問題:這些方式是靜態的,使用者不能控制增加行為的方式和時機;另 外,新功能的存在多種組合,使用繼承可能導致大量子類存在。綜上,MyBtis 快取模組採用 了裝飾器模式實現了快取模組; 

二、裝飾器模式

1、裝飾器模式介紹

裝飾器模式是一種用於代替繼承的技術,無需通過繼承增加子類就能擴充套件物件的新功能。使 用物件的關聯關係代替繼承關係,更加靈活,同時避免型別體系的快速膨脹。裝飾器 UML 類圖如下: \  

元件含義如下:

  • 元件(Component) :元件介面定義了全部元件類和裝飾器實現的行為; 
  • 元件實現類(ConcreteComponent) :實現 Component 介面,元件實現類就是被裝飾器 裝飾的原始物件,新功能或者附加功能都是通過裝飾器新增到該類的物件上的
  • 裝飾器抽象類(Decorator) :實現 Component 介面的抽象類,在其中封裝了一個 Component 物件,也就是被裝飾的物件;
  • 具體裝飾器類(ConcreteDecorator) :該實現類要向被裝飾的物件新增某些功能; 

我們很多人都玩過遊戲,以DNF裡的職業劍魂為例,裝飾器模式圖示如下: \

2、裝飾器模式優點

裝飾器相對於繼承,裝飾器模式靈活性更強,擴充套件性更強:  

  • 靈活性:裝飾器模式將功能切分成一個個獨立的裝飾器,在執行期可以根據需要動態的 新增功能,甚至對新增的新功能進行自由的組合;
  • 擴充套件性:當有新功能要新增的時候,只需要新增新的裝飾器實現類,然後通過組合方式 新增這個新裝飾器,無需修改已有程式碼,符合開閉原則;

3、裝飾器模式使用舉例

  1. IO 中輸入流和輸出流的設計 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream("c://a.txt"))); 
  2. 對網路爬蟲的自定義增強,可增強的功能包括:多執行緒能力、快取、自動生成報表、黑 白名單、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 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 之外,快取模組還有其他的裝飾器如: 

  1. LoggingCache:日誌能力的快取; 
  2. ScheduledCache:定時清空的快取; 
  3. BlockingCache:阻塞式快取; 
  4. SerializedCache:序列化能力的快取; 
  5. SynchronizedCache:進行同步控制的快取; 

那麼問題來了,我們知道HashMap是執行緒不安全的,那麼Mybatis 的快取功能使用 HashMap 實現會不會出現併發安全的問題呢? 

MyBatis 的快取分為一級快取、二級快取。二級快取是多個會話共享的快取,確實會出 現併發安全的問題,因此 MyBatis 在初始化二級快取時,會給二級快取預設加上 SynchronizedCache 裝飾器的增強,在對共享資料 HashMap 操作時進行同步控制,所以二級 快取不會出現併發安全問題;而一級快取是會話獨享的,不會出現多個執行緒同時操作快取數 據的場景,因此一級快取也不會出現併發安全的問題; 

四、快取的唯一標識 CacheKey

MyBatis 中涉及到動態 SQL 的原因,快取項的 key 不能僅僅通過一個 String 來表示,所以通 過 CacheKey 來封裝快取的 Key 值,CacheKey 可以封裝多個影響快取項的因素;\ 判斷兩個 CacheKey是否相同關鍵是比較兩個物件的hash值是否一致;構成CacheKey物件的要素包括:

  1. mappedStatment 的 id 
  2. 指定查詢結果集的範圍(分頁資訊) 
  3. 查詢所使用的 SQL 語句 
  4. 使用者傳遞給 SQL 語句的實際引數值

CacheKey 中 update 方法和 equals 方法是進行比較時非常重要的兩個方法:

  • update 方法:用於新增構成 CacheKey 物件的要素,每新增一個元素會對 hashcode、checksum、count 以及 updateList 進行更新;
  • equals 方法:用於比較兩個元素是否相等。首先比較 hashcode、checksum、count 是否 相等,如果這三個值相等,會迴圈比較 updateList 中每個元素的 hashCode 是否一致;

按照這種方式判斷兩個物件是否相等,一方面能很嚴格的判斷是否一致避免出現誤判, 另外一方面能提高比較的效率;