Mybatis源碼學習第三課---基礎支持層緩存模塊分析

     MyBatis 作爲一個強大的持久層框架,緩存是其必不可少的功能之一。 MyBatis 中的緩存是兩層結構的,分爲一級緩存、二級緩存,但在本質上是相同的,它們使用的都是 Cache 接 口的實現。

     緩存模式主要是設計模式是裝飾器模式。

在 MyBatis 的緩存模塊中,使用了裝飾器模式的變體,其中將 Decorator 接口和 Component接口合併爲一個 Component 接口,得到的類問結構下圖所示。
   

        使用裝飾器模式的有兩個明顯的優點:

        1、相較於繼承來說,裝飾器模式的靈活性更強,可擴展性也強。正如前面所說,繼承方 式會導致大量子類的情況。而裝飾者模式可以將複雜的功能切分成一個個獨立的裝飾 器,通過多個獨立裝飾器的動態組合,創建不同功能的組件,從而滿足多種不同需求。

       2、當有新功能需要添加時,只需要添加新的裝飾器實現類,然後通過組合方式添加這個 新裝飾器即可,無須修改己有類的代碼,符合“開放一封閉”原則。

     但是,隨着添加的新需求越來越多,可能會創建出嵌套多層裝飾器的對象,這增加了系統 的複雜性, 也增加了理解的難度和定位錯誤的難度。

一、Cache 接口及其實現

    MyBatis 中緩存模塊相關的代碼位於 cache 包下, 其中 Cache 接口是緩存模塊中最核心的接 口,它定義了所有緩存的基本行爲。 Cache 接口的定義如下:

public interface Cache {

  //該緩存對象的id
  String getId();

  //向緩存中添加數據,一般情況下, key 是 CacheKey, value 是查詢結果
  void putObject(Object key, Object value);

  //根據指定的 key,在緩存中查找對應的結果對象
  Object getObject(Object key);

  //刪除 key 對應的緩存項
  Object removeObject(Object key);
  //清空緩存
  void clear();
  //緩存項的個數,該方法不會被 MyBatis 核心代碼使用,所以可提供空實現
  int getSize();
  //獲取讀寫鎖,該方法不會被 MyBatis 核心代碼使用,所以可提供空實現
  default ReadWriteLock getReadWriteLock() {
    return null;
  }

}

Cache 接口的實現類有多個,但大部分都是裝飾器,只有 PerpetualCache 提供了 Cache 接口的基本實現。
               

1.1 Perpetual Cache 

     Perpetual Cache 在緩存模塊中扮演着 ConcreteComponent 的角色,其實現比較簡單,底層使 用 HashMap 記錄緩存項,也是通過該 HashMap 對象的方法實現的 Cache 接口中定義的相應方 法。 PerpetualCache 的具體實現如下:

public class PerpetualCache implements Cache {
  //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();}

  @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();}
// ... 重寫 了 equals ()方法和 hashCode ()方法,兩者都只關心 id 字段,並不關心 cache 字段(略)

}

        下面來介紹 cache.decorators 包下提供的裝飾器,它們都直接實現了 Cache 接口,扮演着 ConcreteDecorator 的角色。這些裝飾器會在 PerpetualCache 的基礎上提供一些額外的功能,通 過多個組合後滿足一個特定的需求,後面介紹二級緩存時,會見到這些裝飾器是如何完成動態 組合的。

1.2 BlockingCache 

      BlockingCache 是阻塞版本的緩存裝飾器,它會保證只有一個線程到數據庫中查找指定 key 對應的數據。屬於細粒度鎖,會阻塞相同key的線程,對於不同key的線程不會阻塞。

  

 BlockingCache 中各個字段的含義如下:

private long timeout;//阻塞的超時時長
  private final Cache delegate;//被裝飾的底層對象,一般是PerpetualCache
  //每個 key 都有對應的 ReentrantLock 對象
  private final ConcurrentHashMap<Object, ReentrantLock> locks;//鎖對象集,粒度到key值

  public BlockingCache(Cache delegate) {
    this.delegate = delegate;
    this.locks = new ConcurrentHashMap<>();
  }

假設線程 A 在 BlockingCache 中未查找到 keyA 對應的緩存項時,線程 A 會獲取 keyA 對應 的鎖,這樣後續線程在查找 keyA 時會發生阻塞,如下圖所示:

                  

BlockingCache.getObject()方法的代碼如下:

 /**
   * 細粒度鎖
   * 在get的時候先去獲得key的鎖
   * @param key The key
   * @return
   */
  @Override
  public Object getObject(Object key) {
    //嘗試去獲取鎖
    acquireLock(key);
    Object value = delegate.getObject(key);
    //緩存有 key 對應的緩存項,擇放鎖,否則繼續持有鎖
    if (value != null) {
      //釋放鎖
      releaseLock(key);
    }
    return value;
  }

  acquireLock(key)會去嘗試獲取當前key的鎖,如果當前key沒有對象鎖則爲其創建新的ReentrantLock對象,在對其加鎖。如果有改key的對象鎖存在則使用該對象,對其加鎖。如果加鎖失敗,阻塞一段時間。其代碼如下:

      

//acquireLock()方法中會嘗試獲取指定 key 對應的鎖。
  // 如果該 key 沒有對應的鎖對象則爲其創建新的 ReentrantLock 對象,再加鎖;如果獲取鎖失敗, 則阻塞一段時間。
  private void acquireLock(Object key) {
    //獲取ReentrantLock 對象
    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) {
    //把新鎖添加到locks集合中,如果添加成功使用新鎖,如果添加失敗則使用locks集合中的鎖
    return locks.computeIfAbsent(key, k -> new ReentrantLock());
  }

          假設線程 A 從數據庫中查找到 keyA 對應的結果對象後,將結果對象放入到 BlockingCache 中,此時線程 A 會釋放 keyA 對應的鎖,喚醒阻塞在該鎖上的線程。其他線程即可從 BlockingCache 中獲取 keyA 對應的數據,而不是再次訪問數據庫

                                                      

BlockingCache.putObject()方法的實現如下

 @Override
  public void putObject(Object key, Object value) {
    try {
      delegate.putObject(key, value);
    } finally {
      releaseLock(key);
    }
  }
  //釋放鎖
  private void releaseLock(Object key) {
    ReentrantLock lock = locks.get(key);
    //鎖是否被當前線程持有
    if (lock.isHeldByCurrentThread()) {
      lock.unlock();
    }
  }

1.3 FifoCache&LruCache 

        在很多場景中,爲了控制緩存大小,系統需要按照一定的規則清楚緩存。fifocache 是先進先出版本的裝飾器,當向緩存添加數據時,如果緩存數據個數已經達到上線,則清除最先進來的緩存數據。

     FifoCache中屬性如下所示:

  private final Cache delegate;//底層被裝飾的cache對象
  //用於記錄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;
  }

FifoCacbe.getObject()和 removeObject()方法的實現都是直接調用底層 Cache 對象的對應方 法, 不再贅述。 在      FifoCacbe.p舊Object()方法中會完成緩存項個數的檢測以及緩存的清理操作, 具體實現如下:

 @Override
  public void putObject(Object key, Object value) {
    cycleKeyList(key);//把key放入隊列中,檢測並清理緩存
    delegate.putObject(key, value);//記錄緩存
  }
  private void cycleKeyList(Object key) {
    keyList.addLast(key);//記錄key
    if (keyList.size() > size) {//如果緩存達到上線則清除最早記錄
      Object oldestKey = keyList.removeFirst();
      delegate.removeObject(oldestKey);
    }
  }

       LruCache 是按照近期最少使用算法(Least Recently Used, LRU)進行緩存清理的裝飾器, 在需要清理緩存時,它會清除最近最少使用的緩存工頁。LruCache 中定義的各個字段的含義如下:

 private final Cache delegate;//被裝飾的底層cache對象
  //LinkedHashMap<Obj ect, Object>類型對象,它是一個有序的 HashMap,用於記錄 key 最近的使用情況
  private Map<Object, Object> keyMap;
  private Object eldestKey;//記錄最少被使用的緩存項的key

  public LruCache(Cache delegate) {
    this.delegate = delegate;
    setSize(1024);
  }

   LruCache 的構造函數中默認設置的緩存大小是 1024,我們可以通過其 setSize()方法重新設 置緩存大小, 具體實現如下:

//重新設置緩存大小時,會重置 keyMap 字段
  public void setSize(final int size) {
    //LinkedHashMap的第三個參數爲true,true表示表示該 LinkedHashMap 記錄的順序是
    // access-order,也就是說 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;
      }
    };
  }

        爲了讓讀者更好地理解  LinkedHashMap,下圖 展示了其原理,圖中的虛線形成了一個隊 列,當 LinkedHashMap.get()方法訪問 K1時,會修改這條虛線隊列將 K1項移動到隊列尾部, LruCache 就是通過 LinkedHashMap 的這種特性來確定最久未使用的緩存項。

        LruCache.getObject()方法除了返回緩存項,還會調用 keyMap.get()方法修改 key 的順序,表 示指定的 key 最近被使用。 LruCache.putObject()方法除了添加緩存項,還會將 eldestKey 宇段指 定的緩存項清除掉。具體實現如下:   

  @Override
  public Object getObject(Object key) {
    keyMap.get(key); //修改key中在記錄中的順序
    return delegate.getObject(key);
  }

  @Override
  public void putObject(Object key, Object value) {
    delegate.putObject(key, value);//加入緩存
    cycleKeyList(key);//把key加入記錄中並會清除緩存
  }
  private void cycleKeyList(Object key) {
    //
    keyMap.put(key, key);
    if (eldestKey != null) {//eldestKey 不爲空,表示已經達到緩存上
      delegate.removeObject(eldestKey);//清除最少使用的緩存
      eldestKey = null;
    }
  }

1.4 SoftCache&WeakCache 

    Java 提供的 4 種引 用類型,它們分別是強引用 (Strong Reference)、軟引用 ( soft Reference)、弱引用(Weak Reference) 和幽靈引用(Phantom Reference)。詳情可以自行了解。

    SoftCache的實現。SoftCache中各個字段的 含義如下:

 // 在 SoftCache 中,最近使用的一部分緩存項不會被 GC 回收,
  // 這就是通過將其 value 添加到 II hardLinksToAvoidGarbageCollection
  // 集合中實現的(即有強引用指向其 value) II hardLinksToAvoidGarbageCollection 集合是 LinkedList<Object>類型
  private final Deque<Object> hardLinksToAvoidGarbageCollection;
  //ReferenceQueue,引用隊列,用於記錄已經被GC回收的緩存項所對應的SoftEntry對象
  private final ReferenceQueue<Object> queueOfGarbageCollectedEntries;
  private final Cache delegate;
  private int numberOfHardLinks;//強連接的個數, 默認值是 256

  public SoftCache(Cache delegate) {
    this.delegate = delegate;
    this.numberOfHardLinks = 256;
    this.hardLinksToAvoidGarbageCollection = new LinkedList<>();
    this.queueOfGarbageCollectedEntries = new ReferenceQueue<>();
  }

   SoftCache 中緩存項的 value 是 SoftEntry對象, SoftEntry 繼承了 SoftReference<Object>, 其中指向 key 的引用是強引用, 而指向 value 的引用是軟引用 。 SoftEntry對象的實現如下:

  private static class SoftEntry extends SoftReference<Object> {
    private final Object key;
    //
    SoftEntry(Object key, Object value, ReferenceQueue<Object> garbageCollectionQueue) {
      super(value, garbageCollectionQueue);//指向 value 的引用是軟引用,且關聯了引用隊列 
      this.key = key;///key是強引用
    }
  }

SoftCache.putObject()方法除了向緩存中添加緩存項,還會清除己經被 GC 回收的緩存項, 其具體實現如下:

  @Override
  public void putObject(Object key, Object value) {
    //清除已經被 GC 回收的緩存項
    removeGarbageCollectedItems();
    //向緩存中添加緩存項
    delegate.putObject(key, new SoftEntry(key, value, queueOfGarbageCollectedEntries));
  }
  //下面是 removeGarbageCollecteditems()方法的實現:
  private void removeGarbageCollectedItems() {
    SoftEntry sv;
    //遍歷queueOfGarbageCollectedEntries 集合
    while ((sv = (SoftEntry) queueOfGarbageCollectedEntries.poll()) != null) {
      delegate.removeObject(sv.key);//將已經被 GC 回收的 value 對象對應的緩存項清除
    }
  }

     SoftCache.getObject()方法除了從緩存中查找對應的 value,處理被 GC 回收的 value 對應的 緩存項, 還會更新 hardLinksToAvoidGarbageCollection 集合, 具體實現如下:   

@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) {//已經被 GC 回收
        delegate.removeObject(key);//從緩存中清除對應的緩存項 
      } else {
        ///緩存項的 value 添加到 hardLinksToAvoidGarbageCollection 集合中保存
        // See #586 (and #335) modifications need more than a read lock
        synchronized (hardLinksToAvoidGarbageCollection) {
          hardLinksToAvoidGarbageCollection.addFirst(result);
          if (hardLinksToAvoidGarbageCollection.size() > numberOfHardLinks) {
            hardLinksToAvoidGarbageCollection.removeLast();
          }
        }
      }
    }
    return result;
  }

1.5 ScheduledCache&LoggingCache&Synchronized&CacheSerializedCache 

            ScheduledCache 是週期性清理緩存的裝飾器,它的 clear Interval 宇段記錄了兩次緩存清理之 間的時間間隔,默認是一小時, lastClear 字段記錄了最近一次清理的時間戳。 ScheduledCache 的 getObject()、 putObject()、 removeObject()等核心方法在執行時,都會根據這兩個字段檢測是 否需要進行清理操作,清理操作會清空緩存中所有緩存項。

             LoggingCache 在 Cache 的基礎上提供了日誌功能,它通過 hit 宇段和 request 字段記錄了 Cache 的命中次數和訪問次數。在 LoggingCache.getO均ect()方法中會統計命中次數和訪問次數 這兩個指標,井按照指定的日誌輸出方式輸出命中率。 LoggingCache 代碼比較簡單,請讀者參 考代碼學習。

             SynchronizedCache通過在每個方法上添加 synchronized關鍵字,爲Cache添加了同步功能, 有點類似於 JDK 中 Collections 中的 SynchronizedCollection 內部類的實現。 SynchronizedCache 代碼比較簡單。 

        Serialized Cache 提供了將 value 對象序列化的功能。 S巳rializedCache 在添加緩存項時,會將 value 對應的 Java 對象進行序列化,井將序列化後的 byte[]數組作爲 value 存入緩存 。 Serialized Cache 在獲取緩存項時,會將緩存項中的 byte[]數組反序列化成 Java 對象。使用前面 介紹的 Cache 裝飾器實現進行裝飾之後,每次從緩存中獲取同- key 對應的對象時,得到的都 是同一對象,任意一個線程修改該對象都會影 響到其他線程以及緩存中的對象:而 SerializedCache 每次從緩存中獲取數據時,都會通過反序列化得到一個全新的對象。 

      二、CacheKey

        在 Cache 中唯一確定一個緩存項需要使用緩存項的 key, MyBatis 中因爲涉及動態 SQL 等 多方面因素, 其緩存項的 key 不能僅僅通過一個 String 表示,所以 MyBatis 提供了 CacheKey 類來表示緩存項的 key,在一個 CacheKey 對象中可以封裝多個影響緩存項的因素。 CacheKey 中可以添加多個對象,由這些對象共同確定兩個 CacheKey 對象是否相同。

    CacheKey中的核心字段:

  private static final int DEFAULT_MULTIPLIER = 37;
  private static final int DEFAULT_HASHCODE = 17;

  private final int multiplier;//參與hash計算的乘數 默認是37
  private int hashcode;//CacheKey的hash值,在update函數中實時運算出來的
  private long checksum;//校驗和
  private int count;//updateList集合的個數
  private List<Object> updateList;//由該集合中的所有對象共同決定兩個 CacheKey 是否相同

  public CacheKey() {
    this.hashcode = DEFAULT_HASHCODE;
    this.multiplier = DEFAULT_MULTIPLIER;
    this.count = 0;
    this.updateList = new ArrayList<>();
  }

     CacheKey 對象由四個部分構成,也就是說這四部分都 會記錄到該 CacheKey 對象的 updateList 集合中:

        1、MappedStatement 的 id。

        2、指定查詢結果集的範圍,也就是 RowBounds.offset 和 RowBounds.limit。

       3、查詢所使用的 SQL 語句,也就是 boundSql.getSql()方法返回的 SQL 語句,其中可能包 含“?”佔位符。

       4、用戶傳遞給上述 SQL 語句的實際參數值。

      在向 CacheKey.updateList 集合中添加對象時,使用的是 CacheKey.update()方法,具體實現 如下:

 public CacheKey(Object[] objects) {
    this();
    updateAll(objects);
  }
public void updateAll(Object[] objects) {
    for (Object o : objects) {
      update(o);
    }
  }
  public void update(Object object) {
    //獲取object的hash值
    int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
    //更新count、checksum以及hashcode的值
    count++;
    checksum += baseHashCode;
    baseHashCode *= count;
    hashcode = multiplier * hashcode + baseHashCode;
    //將對象添加到updateList中
    updateList.add(object);
  }

       CacheKey 重寫了 equals()方法和 hashCode()方法,這兩個方法使用上面介紹的 count、 checksum、 hashcode、 updateList 比較 CacheKey 對象是否相同,具體實現如下:

@Override
  public boolean equals(Object object) {
    if (this == object) { //比較是否是同一對象
      return true;
    }
    if (!(object instanceof CacheKey)) {//判斷是否是同一類型
      return false;
    }

    final CacheKey cacheKey = (CacheKey) object;

    if (hashcode != cacheKey.hashcode) {//判斷hashcode是否相同
      return false;
    }
    if (checksum != cacheKey.checksum) {////checksum是否相同
      return false;
    }
    if (count != cacheKey.count) {//count是否相同
      return false;
    }
    //順序比較updateList中元素的hash值是否一致
    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;
  }

三 cache裝飾者類的使用

CacheBuilder.setStandardDecorators()方法會根據 CacheBuilder 中各個字段的值,爲 cache 對 象添加對應的裝飾器,具體實現如下:

  

 //緩存模塊裝飾器的使用
  private Cache setStandardDecorators(Cache cache) {
    try {
      //獲取基本的cache對象
      MetaObject metaCache = SystemMetaObject.forObject(cache);
      //緩存的容量大小
      if (size != null && metaCache.hasSetter("size")) {
        metaCache.setValue("size", size);
      }
      //檢測是否指定了 clearinterval 字段 (定期清除)
      if (clearInterval != null) {
        cache = new ScheduledCache(cache);//添加定期清除的裝飾器
        ((ScheduledCache) cache).setClearInterval(clearInterval);
      }
      //如果是隻讀緩存
      if (readWrite) {
        //是否只讀,對應添加 SerializedCache 裝飾器
          cache = new SerializedCache(cache);
      }
      //默認添加 LoggingCache 和 SynchronizedCache 兩個裝飾器
      cache = new LoggingCache(cache);
      cache = new SynchronizedCache(cache);
      if (blocking) {//阻塞裝飾器
        cache = new BlockingCache(cache);
      }
      return cache;
    } catch (Exception e) {
      throw new CacheException("Error building standard cache decorators.  Cause: " + e, e);
    }
  }

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章