Mybatis基礎支持層源碼閱讀分析(緩存模塊)

MyBatis 中的緩存分爲一級緩存、二級緩存,但在本質上是相同的,它們使用的都是 Cache接口 的實現。MyBatis緩存模塊 的設計,使用了裝飾器模式,這裏不對此進行過多解析,以後會專門開一篇博文分析常用框架中使用到的設計模式。

1 Cache組件

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

public interface Cache {

  /**
   * 獲取當前緩存的 Id
   */
  String getId();

  /**
   * 存入緩存的 key 和 value,key 一般爲 CacheKey對象
   */
  void putObject(Object key, Object value);

  /**
   * 根據 key 獲取緩存值
   */
  Object getObject(Object key);

  /**
   * 刪除指定的緩存項
   */
  Object removeObject(Object key);

  /**
   * 清空緩存
   */
  void clear();

  /**
   * 獲取緩存的大小
   */
  int getSize();

  /**
   * !!!!!!!!!!!!!!!!!!!!!!!!!!
   * 獲取讀寫鎖,可以看到,這個接口方法提供了默認的實現!!
   * 這是 Java8 的新特性!!只是平時開發時很少用到!!!
   * !!!!!!!!!!!!!!!!!!!!!!!!!!
   */
  default ReadWriteLock getReadWriteLock() {
    return null;
  }
}

如下圖所示,Cache接口 的實現類有很多,但大部分都是裝飾器,只有 PerpetualCache 提供了 Cache 接口 的基本實現。
在這裏插入圖片描述

1.1 PerpetualCache

PerpetualCache(Perpetual:永恆的,持續的)在緩存模塊中扮演着被裝飾的角色,其實現比較簡單,底層使用 HashMap 記錄緩存項,也是通過該 HashMap對象 的方法實現的 Cache接口 中定義的相應方法。

public class PerpetualCache implements Cache {

  // Cache對象 的唯一標識
  private final String id;

  // 其所有的緩存功能實現,都是基於 JDK 的 HashMap 提供的方法
  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();
  }

  /**
   * 其重寫了 Object 中的 equals() 和 hashCode()方法,兩者都只關心 id字段
   */
  @Override
  public boolean equals(Object o) {
    if (getId() == null) {
      throw new CacheException("Cache instances require an ID.");
    }
    if (this == o) {
      return true;
    }
    if (!(o instanceof Cache)) {
      return false;
    }

    Cache otherCache = (Cache) o;
    return getId().equals(otherCache.getId());
  }

  @Override
  public int hashCode() {
    if (getId() == null) {
      throw new CacheException("Cache instances require an ID.");
    }
    return getId().hashCode();
  }
}

下面來看一下 cache.decorators包 下提供的裝飾器,它們都直接實現了 Cache接口,扮演着裝飾器的角色。這些裝飾器會在 PerpetualCache 的基礎上提供一些額外的功能,通過多個組合後滿足一個特定的需求。

1.2 BlockingCache

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

public class BlockingCache implements Cache {

  // 阻塞超時時長
  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<>();
  }
}

假設 線程A 在 BlockingCache 中未查找到 keyA 對應的緩存項時,線程A 會獲取 keyA 對應的鎖,這樣,線程A 在後續查找 keyA 時,其它線程會被阻塞。

  // 根據 key 獲取鎖對象,然後上鎖
  private void acquireLock(Object key) {
    // 獲取 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 ReentrantLock getLockForKey(Object key) {
    // Java8 新特性,Map系列類 中新增的方法
    // V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
    // 表示,若 key 對應的 value 爲空,則將第二個參數的返回值存入該 Map集合 並返回
    return locks.computeIfAbsent(key, k -> new ReentrantLock());
  }

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

  @Override
  public void putObject(Object key, Object value) {
    try {
      // 存入 key 和其對應的緩存項
      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 是先入先出版本的裝飾器,當向緩存添加數據時,如果緩存項的個數已經達到上限,則會將緩存中最老(即最早進入緩存)的緩存項刪除。

public class FifoCache implements Cache {

  // 被裝飾對象
  private final Cache delegate;
  // 用一個 FIFO 的隊列記錄 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;
  }

  @Override
  public String getId() {
    return delegate.getId();
  }

  @Override
  public int getSize() {
    return delegate.getSize();
  }

  public void setSize(int size) {
    this.size = size;
  }

  @Override
  public void putObject(Object key, Object value) {
    // 存儲緩存項之前,先在 keyList 中註冊
    cycleKeyList(key);
    // 存儲緩存項
    delegate.putObject(key, value);
  }

  private void cycleKeyList(Object key) {
    // 在 keyList隊列 中註冊要添加的 key
    keyList.addLast(key);
    // 如果註冊這個 key 會超出容積上限,則把最老的一個緩存項清除掉
    if (keyList.size() > size) {
      Object oldestKey = keyList.removeFirst();
      delegate.removeObject(oldestKey);
    }
  }

  @Override
  public Object getObject(Object key) {
    return delegate.getObject(key);
  }

  @Override
  public Object removeObject(Object key) {
    return delegate.removeObject(key);
  }

  // 除了清理緩存項,還要清理 key 的註冊列表
  @Override
  public void clear() {
    delegate.clear();
    keyList.clear();
  }

}

LruCache 是按照"近期最少使用算法"(Least Recently Used, LRU)進行緩存清理的裝飾器,在需要清理緩存時,它會清除最近最少使用的緩存項。

public class LruCache implements Cache {

  // 被裝飾者
  private final Cache delegate;
  // 這裏使用的是 LinkedHashMap,它繼承了 HashMap,但它的元素是有序的
  private Map<Object, Object> keyMap;
  // 最近最少被使用的緩存項的 key
  private Object eldestKey;

  // 國際慣例,構造方法中進行屬性初始化
  public LruCache(Cache delegate) {
    this.delegate = delegate;
    // 這裏初始化了 keyMap,並定義了 eldestKey 的取值規則
    setSize(1024);
  }

  public void setSize(final int size) {
    // 初始化 keyMap,同時指定該 Map 的初始容積及加載因子,第三個參數true 表示 該LinkedHashMap
    // 記錄的順序是 accessOrder,即,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;
      }
    };
  }

  // 存儲緩存項
  @Override
  public void putObject(Object key, Object value) {
    delegate.putObject(key, value);
    // 記錄緩存項的 key,超出容量則清除最久未使用的緩存項
    cycleKeyList(key);
  }

  private void cycleKeyList(Object key) {
    keyMap.put(key, key);
    // eldestKey 不爲空,則表示已經達到緩存上限
    if (eldestKey != null) {
      // 清除最久未使用的緩存
      delegate.removeObject(eldestKey);
      // 制空
      eldestKey = null;
    }
  }

  @Override
  public Object getObject(Object key) {
    // 訪問 key元素 會改變該元素在 LinkedHashMap 中的順序
    keyMap.get(key); //touch
    return delegate.getObject(key);
  }

  @Override
  public String getId() {
    return delegate.getId();
  }

  @Override
  public int getSize() {
    return delegate.getSize();
  }

  @Override
  public Object removeObject(Object key) {
    return delegate.removeObject(key);
  }

  @Override
  public void clear() {
    delegate.clear();
    keyMap.clear();
  }

}

1.4 SoftCache和WeakCache

在分析 SoftCache 和 WeakCache 實現之前,我們再溫習一下 Java 提供的4種引用類型,強引用StrongReference、軟引用SoftReference、弱引用WeakReference和虛引用PhantomReference。

  • 強引用
    平時用的最多的,如 Object obj = new Object(),新建的 Object對象 就是被強引用的。如果一個對象被強引用,即使是 JVM內存空間不足,要拋出 OutOfMemoryError異常,GC 也絕不會回收該對象。
  • 軟引用
    僅次於強引用的一種引用,它使用類 SoftReference 來表示。當 JVM內存不足時,GC 會回收那些只被軟引用指向的對象,從而避免內存溢出。軟引用適合引用那些可以通過其他方式恢復的對象,例如, 數據庫緩存中的對象就可以從數據庫中恢復,所以軟引用可以用來實現緩存,下面要介紹的 SoftCache 就是通過軟引用實現的。
    另外,由於在程序使用軟引用之前的某個時刻,其所指向的對象可能己經被 GC 回收掉了,所以通過 Reference.get()方法 來獲取軟引用所指向的對象時,總是要通過檢查該方法返回值是否爲 null,來判斷被軟引用的對象是否還存活。
  • 弱引用
    弱引用使用 WeakReference表示,它不會阻止所引用的對象被 GC回收。在 JVM 進行垃圾回收時,如果指向一個對象的所有引用都是弱引用,那麼該對象會被回收。
    所以,只被弱引用所指向的對象,其生存週期是 兩次GC之間 的這段時間,而只被軟引用所指向的對象可以經歷多次 GC,直到出現內存緊張的情況才被回收。
  • 虛引用
    最弱的一種引用類型,由類 PhantomReference 表示。虛引用可以用來實現比較精細的內存使用控制,但很少使用。
  • 引用隊列(ReferenceQueue )
    很多場景下,我們的程序需要在一個對象被 GC 時得到通知,引用隊列就是用於收集這些信息的隊列。在創建 SoftReference對象 時,可以爲其關聯一個引用隊列,當 SoftReference 所引用的對象被 GC 時, JVM 就會將該 SoftReference對象 添加到與之關聯的引用隊列中。當需要檢測這些通知信息時,就可以從引用隊列中獲取這些 SoftReference對象。不僅是 SoftReference,弱引用和虛引用都可以關聯相應的隊列。

現在來看一下 SoftCache 的具體實現。

public class SoftCache implements Cache {

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

  // 構造方法進行屬性的初始化
  public SoftCache(Cache delegate) {
    this.delegate = delegate;
    this.numberOfHardLinks = 256;
    this.hardLinksToAvoidGarbageCollection = new LinkedList<>();
    this.queueOfGarbageCollectedEntries = new ReferenceQueue<>();
  }

  private static class SoftEntry extends SoftReference<Object> {
    private final Object key;

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

  @Override
  public void putObject(Object key, Object value) {
    // 清除已經被 GC 的緩存項
    removeGarbageCollectedItems();
    // 添加緩存
    delegate.putObject(key, new SoftEntry(key, value, queueOfGarbageCollectedEntries));
  }

  private void removeGarbageCollectedItems() {
    SoftEntry sv;
    // 遍歷 queueOfGarbageCollectedEntries集合,清除已經被 GC 的緩存項 value
    while ((sv = (SoftEntry) queueOfGarbageCollectedEntries.poll()) != null) {
      delegate.removeObject(sv.key);
    }
  }

  @Override
  public Object getObject(Object key) {
    Object result = null;
    @SuppressWarnings("unchecked") // assumed delegate cache is totally managed by this cache
      // 用一個軟引用指向 key 對應的緩存項
      SoftReference<Object> softReference = (SoftReference<Object>) delegate.getObject(key);
    // 檢測緩存中是否有對應的緩存項
    if (softReference != null) {
      // 獲取 softReference 引用的 value
      result = softReference.get();
      // 如果 softReference 引用的對象已經被 GC,則從緩存中清除對應的緩存項
      if (result == null) {
        delegate.removeObject(key);
      } else {
        synchronized (hardLinksToAvoidGarbageCollection) {
          // 將緩存項的 value 添加到 hardLinksToAvoidGarbageCollection集合 中保存
          hardLinksToAvoidGarbageCollection.addFirst(result);
          // 如果 hardLinksToAvoidGarbageCollection 的容積已經超過 numberOfHardLinks
          // 則將最老的緩存項從 hardLinksToAvoidGarbageCollection 中清除,FIFO
          if (hardLinksToAvoidGarbageCollection.size() > numberOfHardLinks) {
            hardLinksToAvoidGarbageCollection.removeLast();
          }
        }
      }
    }
    return result;
  }

  @Override
  public Object removeObject(Object key) {
    // 清除指定的緩存項之前,也會先清理被 GC 的緩存項
    removeGarbageCollectedItems();
    return delegate.removeObject(key);
  }


  @Override
  public void clear() {
    synchronized (hardLinksToAvoidGarbageCollection) {
      // 清理強引用集合
      hardLinksToAvoidGarbageCollection.clear();
    }
    // 清理被 GC 的緩存項
    removeGarbageCollectedItems();
    // 清理最底層的緩存項
    delegate.clear();
  }

  @Override
  public String getId() {
    return delegate.getId();
  }

  @Override
  public int getSize() {
    removeGarbageCollectedItems();
    return delegate.getSize();
  }

  public void setSize(int size) {
    this.numberOfHardLinks = size;
  }

}

WeakCache 的實現與 SoftCache 基本類似,唯一的區別在於其中使用 WeakEntry(繼承了WeakReference)封裝真正的 value對象,其他實現完全一樣。

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

LoggingCache 在 Cache 的基礎上提供了日誌功能,它通過 hit字段 和 request字段 記錄了 Cache 的命中次數和訪問次數。在 LoggingCache.getObject()方法 中,會統計命中次數和訪問次數 這兩個指標,井按照指定的日誌輸出方式輸出命中率。

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

SerializedCache 提供了將 value對象 序列化的功能。SerializedCache 在添加緩存項時,會將 value 對應的 Java對象 進行序列化,井將序列化後的 byte[]數組 作爲 value 存入緩存 。 SerializedCache 在獲取緩存項時,會將緩存項中的 byte[]數組 反序列化成 Java對象。不使用 SerializedCache裝飾器 進行裝飾的話,每次從緩存中獲取同一 key 對應的對象時,得到的都是同一對象,任意一個線程修改該對象都會影響到其他線程,以及緩存中的對象。而使用 SerializedCache 每次從緩存中獲取數據時,都會通過反序列化得到一個全新的對象。 SerializedCache 使用的序列化方式是 Java原生序列化。

2 CacheKey

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

public class CacheKey implements Cloneable, Serializable {

  private static final long serialVersionUID = 1146682552656046210L;

  public static final CacheKey NULL_CACHE_KEY = new NullCacheKey();

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

  // 參與計算hashcode,默認值DEFAULT_MULTIPLYER = 37
  private final int multiplier;
  // 當前CacheKey對象的hashcode,默認值DEFAULT_HASHCODE = 17
  private int hashcode;
  // 校驗和
  private long checksum;
  private int count;

  // 由該集合中的所有元素 共同決定兩個CacheKey對象是否相同,一般會使用一下四個元素
  // MappedStatement的id、查詢結果集的範圍參數(RowBounds的offset和limit)
  // SQL語句(其中可能包含佔位符"?")、SQL語句中佔位符的實際參數
  private List<Object> updateList;

  // 構造方法初始化屬性
  public CacheKey() {
    this.hashcode = DEFAULT_HASHCODE;
    this.multiplier = DEFAULT_MULTIPLYER;
    this.count = 0;
    this.updateList = new ArrayList<>();
  }

  public CacheKey(Object[] objects) {
    this();
    updateAll(objects);
  }

  public void update(Object object) {
    int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
    // 重新計算count、checksum和hashcode的值
    count++;
    checksum += baseHashCode;
    baseHashCode *= count;
    hashcode = multiplier * hashcode + baseHashCode;
    // 將object添加到updateList集合
    updateList.add(object);
  }

  public int getUpdateCount() {
    return updateList.size();
  }

  public void updateAll(Object[] objects) {
    for (Object o : objects) {
      update(o);
    }
  }

  /**
   * CacheKey重寫了 equals() 和 hashCode()方法,這兩個方法使用上面介紹
   * 的 count、checksum、hashcode、updateList 比較兩個 CacheKey對象 是否相同
   */
  @Override
  public boolean equals(Object object) {
    // 如果爲同一對象,直接返回 true
    if (this == object) {
      return true;
    }
    // 如果 object 都不是 CacheKey類型,直接返回 false
    if (!(object instanceof CacheKey)) {
      return false;
    }

    // 類型轉換一下
    final CacheKey cacheKey = (CacheKey) object;

    // 依次比較 hashcode、checksum、count,如果不等,直接返回 false
    if (hashcode != cacheKey.hashcode) {
      return false;
    }
    if (checksum != cacheKey.checksum) {
      return false;
    }
    if (count != cacheKey.count) {
      return false;
    }

    // 比較 updateList 中的元素是否相同,不同直接返回 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;
  }

  @Override
  public int hashCode() {
    return hashcode;
  }

  @Override
  public String toString() {
    StringJoiner returnValue = new StringJoiner(":");
    returnValue.add(String.valueOf(hashcode));
    returnValue.add(String.valueOf(checksum));
    updateList.stream().map(ArrayUtil::toString).forEach(returnValue::add);
    return returnValue.toString();
  }

  @Override
  public CacheKey clone() throws CloneNotSupportedException {
    CacheKey clonedCacheKey = (CacheKey) super.clone();
    clonedCacheKey.updateList = new ArrayList<>(updateList);
    return clonedCacheKey;
  }

}

3 小結

至此 Mybatis 的基礎支持層的主要模塊就分析完了。本模塊首先介紹了 MyBatis 對 Java反射機制的封裝;然後分析了類型轉換 TypeHandler組件,瞭解了 MyBatis 如何實現數據在 Java類型 與 JDBC類型 之間的轉換。

之後分析了 MyBatis 提供的 DataSource模塊 的實現和原理,深入解析了 MyBatis 自帶的連接池 PooledDataSource 的詳細實現;後面緊接着介紹了 Transaction模塊 的功能。然後分析了 binding模塊 如何將 Mapper接口 與映射配置信息相關聯,以及其中的原理。最後介紹了 MyBatis 的緩存模塊,分析了 Cache接口 以及多個實現類的具體實現,它們是 Mybatis 中一級緩存和二級緩存的基礎。

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