看Mybatis如何花樣設計Cache.md

看Mybatis如何花樣設計 Cache

在這裏插入圖片描述
爲什麼說花樣設計 Cache , 是因爲Mybatis只是對 Map數據結構的封裝, 但是卻實現了很多挺好用的能力。如果單單從設計模式上的角度來,其實就是典型的裝飾器模式, 裝飾器模式其實並不難,所以我們不講設計模式, 本篇文章我們來看看Mybatils 緩存設計巧妙的點。

在這裏插入圖片描述

通過簡單的代碼review來分析下這十個緩存類設計的巧妙點。

一、模式分析

在這裏插入圖片描述
從目錄就很清晰看出,核心就是impl 包下面只有一個,其他都是裝飾器模式,在
decorators 包下

1. Cache

接口設計沒有什麼好講的,提供獲取和添加方法,跟Map接口一樣。 本篇我們要一起Review的類都會實現該接口的。

(這句話簡直就是廢話,大佬勿噴,就是簡單提醒。意思就是其實代碼不難)

public interface Cache {

  String getId();
  
  void putObject(Object key, Object value);
  
  Object getObject(Object key);

  Object removeObject(Object key);

  void clear();

  int getSize();
  
  ReadWriteLock getReadWriteLock();

}

2. PerpetualCache

這個類就是 Mybatis 緩存最底層的設計, 看一下就知道其實是對 Map 的封裝。
其實我們只要知道他是簡單的 HashMap 的封裝就可以了

public class PerpetualCache implements Cache {
  // 唯一標識
  private final String id;
  // 就是一個HashMap結構
  private Map<Object, Object> cache = new HashMap<Object, Object>();

  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();
  }
  // 基本沒啥用,外層誰要用,誰重寫
  @Override
  public ReadWriteLock getReadWriteLock() {
    return null;
  }

  @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();
  }

}

3. 小總結

其實上面就是Mybatis 關於 Cache 的核心實現,其實看到這裏還沒有很多知識點. 那麼我們從中能學到什麼呢? 如果真要找一條學習的點,那麼就是:

設計要面向接口設計,而不是具體實現。 這樣當我們要重寫 Cache ,比如說我們不想底層用 HashMap 來實現了,其實我們只要實現一下 Cache 接口,然後替換掉PerpetualCache就可以了。對於使用者其實並不感知。

二、開始重頭戲

從這裏我們主要一起看下,代碼設計的巧妙之處,一個一個研究下,以下這10個類。看 Mybatis 是如何巧妙設計的。

在這裏插入圖片描述

1. BlockingCache

BlockingCache是一個簡單和低效的Cache的裝飾器,我們主要看幾個重要方法。

public class BlockingCache implements Cache {

  private long timeout;
  //實現Cache接口的緩存對象
  private final Cache delegate;
  //對每個key生成一個鎖對象
  private final ConcurrentHashMap<Object, ReentrantLock> locks;

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

  @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 {
      //釋放鎖。 爲什麼不加鎖? 所以get和put是組合使用的,當get加鎖,如果沒有就查詢數據庫然後put釋放鎖,然後其他線程就可以直接用緩存數據了。
      releaseLock(key);
    }
  }

  @Override
  public Object getObject(Object key) {
    //1. 當要獲取一個key,首先對key進行加鎖操作,如果沒有鎖就加一個鎖,有鎖就直接鎖
    acquireLock(key);
    Object value = delegate.getObject(key);
    if (value != null) {
      //2. 如果緩存命中,就直接解鎖
      releaseLock(key);
    }
    //3. 當value=null, 就是說沒有命中緩存,那麼這個key就會被鎖住,其他線程進來都要等待
    return value;
  }

  @Override
  public Object removeObject(Object key) {
    // 移除key的時候,順便清楚緩存key的鎖對象
    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);
    //如果key對應的鎖存在就返回,沒有就創建一個新的
    return previous == null ? lock : previous;
  }
  
  private void acquireLock(Object key) {
    Lock lock = getLockForKey(key);
    //1. 如果設置超時時間,就可以等待timeout時間(如果超時了報錯)
    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 {
      //2. 如果沒有設置,直接就加鎖(如果這個鎖已經被人用了,那麼就一直阻塞這裏。等待上一個釋放鎖)
      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;
  }  
}

建議看代碼註釋

方法 解釋
acquireLock 加鎖操作
getObject 進來加鎖,如果緩存存在就釋放鎖,不存在就不釋放鎖。
putObject 添加元素並釋放鎖
removeObject 移除key的時候,順便清楚緩存key的鎖對象
getLockForKey 如果key對應的鎖存在就返回,沒有就創建一個新的

思考

  1. 這個因爲每次key請求都會加lock真的會很慢嗎? 我們舉兩種場景。

注意這個加lock並不是對get方法加lock,而是對每個要get的key來加lock。

場景一: 試想一種場景,當有10個線程同時從數據庫查詢一個key爲123的數據時候,當第一個線程來首先從cache中讀取時候,這個時候其他九個線程是會阻塞的,因爲這個key已經被加lock了。當第一個線程get這個key完成時候,其他線程才能繼續走。這種場景來說是不好的,

場景二: 但是當第一個線程來發現cache裏面沒有數據這個時候其他線程會阻塞,而第一個線程會從db中查詢,然後在put到cache裏面。這樣其他9個線程就不需要在去查詢db了,就減少了9次db查詢。

2. FifoCache

FIFO( First Input First Output),簡單說就是指先進先出

如何實現先進先出呢? 其實非常簡單,當put時候,先判斷是否需要執行淘汰策略,如果要執行淘汰,就 移除先進來的。 直接通過 Deque API 來實現先進先出。

  private final Cache delegate;
  private final Deque<Object> keyList;
  private int size;

  public FifoCache(Cache delegate) {
    this.delegate = delegate;
    this.keyList = new LinkedList<Object>();
    this.size = 1024;
  }

@Override
  public void putObject(Object key, Object value) {
  	//1. put時候就判斷是否需要淘汰
    cycleKeyList(key);
    delegate.putObject(key, value);
  }
  private void cycleKeyList(Object key) {
    keyList.addLast(key);
    //1. size默認如果大於1024就開始淘汰
    if (keyList.size() > size) {
      //2. 利用Deque隊列移除第一個。
      Object oldestKey = keyList.removeFirst();
      delegate.removeObject(oldestKey);
    }
  }

3. LoggingCache

從名字上看就是跟日誌有關, LoggingCache 會在 debug級別下把緩存命中率給統計出來,然後通過日誌系統打印出來。

public Object getObject(Object key) {
    requests++;
    final Object value = delegate.getObject(key);
    if (value != null) {
      hits++;
    }
    //1. 打印緩存命中率
    if (log.isDebugEnabled()) {
      log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio());
    }
    return value;
  }

除此之外沒有什麼其他功能。我們主要看下他是如何統計緩存命中率的。其實很簡單。

public class LoggingCache implements Cache {

  private final Log log;
  private final Cache delegate;
  //1. 總請求次數
  protected int requests = 0;
  //2. 命中次數
  protected int hits = 0;
 
  ...
}  

在get請求時候無論是否命中,都自增總請求次數( request ), 當get命中時候自增命中次數( hits )

public Object getObject(Object key) {
	//1. 無論是否命中,都自增總請求次數( `request` )
    requests++;
    final Object value = delegate.getObject(key);
    if (value != null) {
      //2. get命中時候自增命中次數( `hits` )
      hits++;
    }
    if (log.isDebugEnabled()) {
      log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio());
    }
    return value;
  }

然後我們看命中率怎麼算 getHitRatio()

命中率 = 命中次數 / 總請求次數

 private double getHitRatio() {
    return (double) hits / (double) requests;
  }

4. LruCache

LRU是Least Recently Used的縮寫,即最近最少使用。

首先我們看如何實現 LRU 策略。
它其實就是利用 LinkedHashMap來實現 LRU 策略, JDK 提供的 LinkedHashMap天然就支持 LRU 策略。
LinkedHashMap 有一個特點如果開啓LRU策略後,每次獲取到數據後,都會把數據放到最後一個節點,這樣第一個節點肯定是最近最少用的元素。

public V get(Object key) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) == null)
            return null;
        //1. 判斷是否開始LRU策略
        if (accessOrder)
            //2. 開啓就往後面放
            afterNodeAccess(e);
        return e.value;
    }

在這裏插入圖片描述
構造中先聲明LRU淘汰策略,當size()大於構造中聲明的1024就可以在每次
putObject時候將要淘汰的移除掉。這點非常的巧妙,不知道你學習到了沒 ?

在這裏插入圖片描述

5. ScheduledCache

定時刪除,設計巧妙,可以借鑑。

public class ScheduledCache implements Cache {

  private final Cache delegate;
  protected long clearInterval;
  protected long lastClear;

  public ScheduledCache(Cache delegate) {
    this.delegate = delegate;
    //1. 指定多久清理一次緩存
    this.clearInterval = 60 * 60 * 1000; // 1 hour
    //2. 設置初始值
    this.lastClear = System.currentTimeMillis();
  }

  public void setClearInterval(long clearInterval) {
    this.clearInterval = clearInterval;
  }

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

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

  @Override
  public void putObject(Object key, Object object) {
    clearWhenStale();
    delegate.putObject(key, object);
  }

  @Override
  public Object getObject(Object key) {
    return clearWhenStale() ? null : delegate.getObject(key);
  }

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

  @Override
  public void clear() {
    //1. 記錄最近刪除一次時間戳
    lastClear = System.currentTimeMillis();
    //2. 清理掉緩存信息
    delegate.clear();
  }

  @Override
  public ReadWriteLock getReadWriteLock() {
    return null;
  }

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

  @Override
  public boolean equals(Object obj) {
    return delegate.equals(obj);
  }

  private boolean clearWhenStale() {
    if (System.currentTimeMillis() - lastClear > clearInterval) {
      clear();
      return true;
    }
    return false;
  }

}

核心代碼

  1. 構造中指定多久清理一次緩存(1小時)
  2. 設置初始值
  3. clearWhenStale() 核心方法
  4. 然後在每個方法中調用一次這段代碼,判斷是否需要清理。
private boolean clearWhenStale() {
    //1. 當前時間 - 最後清理時間,如果大於定時刪除時間,說明要執行清理了。
    if (System.currentTimeMillis() - lastClear > clearInterval) {
      clear();
      return true;
    }
    return false;
  }

6. SerializedCache

從名字上看就是支持序列化的緩存,那麼我們就要問了,爲啥要支持序列化?

爲啥要支持序列化?

因爲如果多個用戶同時共享一個數據對象時,同時都引用這一個數據對象。如果有用戶修改了這個數據對象,那麼其他用戶拿到的就是已經修改過的對象,這樣就是出現了線程不安全。

如何解決這種問題

  1. 加鎖當一個線程在操作時候,其他線程不允許操作
  2. 新生成一個對象,這樣多個線程獲取到的數據就不是一個對象了。

只看一下核心代碼

  1. putObject 將對象序列化成byte[]
  2. getObjectbyte[]反序列化成對象
public void putObject(Object key, Object object) {
    if (object == null || object instanceof Serializable) {
      //1. 將對象序列化成byte[]
      delegate.putObject(key, serialize((Serializable) object));
    } else {
      throw new CacheException("SharedCache failed to make a copy of a non-serializable object: " + object);
    }
  }
private byte[] serialize(Serializable value) {
    try {
      ByteArrayOutputStream bos = new ByteArrayOutputStream();
      ObjectOutputStream oos = new ObjectOutputStream(bos);
      oos.writeObject(value);
      oos.flush();
      oos.close();
      return bos.toByteArray();
    } catch (Exception e) {
      throw new CacheException("Error serializing object.  Cause: " + e, e);
    }
  }

 public Object getObject(Object key) {
    Object object = delegate.getObject(key);
    //1. 獲取時候將byte[]反序列化成對象
    return object == null ? null : deserialize((byte[]) object);
  }
  private Serializable deserialize(byte[] value) {
    Serializable result;
    try {
      ByteArrayInputStream bis = new ByteArrayInputStream(value);
      ObjectInputStream ois = new CustomObjectInputStream(bis);
      result = (Serializable) ois.readObject();
      ois.close();
    } catch (Exception e) {
      throw new CacheException("Error deserializing object.  Cause: " + e, e);
    }
    return result;
  }

這種就類似於深拷貝,因爲簡單的淺拷貝會出現線程安全問題,而這種辦法,因爲字節在被反序列化時,會在創建一個新的對象,這個新的對象的數據和原來對象的數據一模一樣。所以說跟深拷貝一樣。

Java開發之深淺拷貝

7. SoftCache

從名字上看,Soft其實就是軟引用。軟引用就是如果內存夠,GC就不會清理內存,只有當內存不夠用了會出現OOM時候,纔開始執行GC清理。

如果要看明白這個源碼首先要先了解一點垃圾回收,垃圾回收的前提是還有沒有別的地方在引用這個對象了。如果沒有別的地方在引用就可以回收了。
本類中爲了阻止被回收所以聲明瞭一個變量hardLinksToAvoidGarbageCollection
也指定了一個將要被回收的垃圾隊列queueOfGarbageCollectedEntries

這個類的主要內容是當緩存value已經被垃圾回收了,就自動把key也清理。

Mybatis 在實際中並沒有使用這個類。

public class SoftCache implements Cache {
  private final Deque<Object> hardLinksToAvoidGarbageCollection;
  private final ReferenceQueue<Object> queueOfGarbageCollectedEntries;
  private final Cache delegate;
  private int numberOfHardLinks;

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

先看下變量聲明

hard Links To Avoid Garbage Collection
硬連接,避免垃圾收集
queue Of Garbage Collected Entries
垃圾要收集的隊列
number Of Hard Links
硬連接數量

@Override
  public void putObject(Object key, Object value) {
    //1. 清除已經被垃圾回收的key
    removeGarbageCollectedItems();
    //2. 注意看SoftEntry(),聲明一個SoftEnty對象,指定垃圾回收後要進入的隊列
    //3. 當SoftEntry中數據要被清理,會添加到類中聲明的垃圾要收集的隊列中
    delegate.putObject(key, new SoftEntry(key, value, queueOfGarbageCollectedEntries));
  }

  @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) {
        //1. 如果數據已經沒有了,就清理這個key
        delegate.removeObject(key);
      } else {
        // See #586 (and #335) modifications need more than a read lock 
        synchronized (hardLinksToAvoidGarbageCollection) {
          //2. 如果key存在,讀取時候加一個鎖操作,並將緩存值添加到硬連接集合中,避免垃圾回收
          hardLinksToAvoidGarbageCollection.addFirst(result);
          //3. 構造中指定硬鏈接最大256,所以如果已經有256個key的時候回開始刪除最先添加的key
          if (hardLinksToAvoidGarbageCollection.size() > numberOfHardLinks) {
            hardLinksToAvoidGarbageCollection.removeLast();
          }
        }
      }
    }
    return result;
  }

  @Override
  public void clear() {
    //執行三清
    synchronized (hardLinksToAvoidGarbageCollection) {
      //1.清除硬鏈接隊列
      hardLinksToAvoidGarbageCollection.clear();
    }
    //2. 清除垃圾隊列
    removeGarbageCollectedItems();
    //3. 清除緩存
    delegate.clear();
  }

  private void removeGarbageCollectedItems() {
    SoftEntry sv;
    //清除value已經gc準備回收了,就就將key也清理掉
    while ((sv = (SoftEntry) queueOfGarbageCollectedEntries.poll()) != null) {
      delegate.removeObject(sv.key);
    }
  }

8. SynchronizedCache

從名字看就是同步的緩存,從代碼看即所有的方法都被synchronized修飾。

在這裏插入圖片描述

9. TransactionalCache

從名字上看就應該能隱隱感覺到跟事務有關,但是這個事務呢又不是數據庫的那個事務。只是類似而已是, 即通過 java 代碼來實現了一個暫存區域,如果事務成功就添加緩存,事務失敗就回滾掉或者說就把暫存區的信息刪除,不進入真正的緩存裏面。 這個類是比較重要的一個類,因爲所謂的二級緩存就是指這個類。既然說了🎧緩存就順便提一下一級緩存。但是說一級緩存就設計到 Mybatis架構裏面一個 Executor 執行器
在這裏插入圖片描述

所有的查詢都先從一級緩存中查詢
在這裏插入圖片描述

在這裏插入圖片描述

看到這裏不由己提一個面試題,面試官會問你知道Mybatis 的一級緩存嗎?
一般都會說Mybatis 的一級緩存就是 SqlSession 自帶的緩存,這麼說也對就是太籠統了,因爲 SqlSession其實就是生成 Executor 而一級緩存就是裏面query方法中的 localCache。這個時候我們就要看下了localCache 究竟是什麼?
看一下構造,突然豁然開朗。原來本篇文章講的基本就是一級緩存的實現呀。
在這裏插入圖片描述

說到這裏感覺有點跑題了,我們不是要看 TransactionalCache 的實現嗎?

clearOnCommit 爲false就是這個事務已經完成了,可以從緩存中讀取數據了。

clearOnCommittrue ,這個事務正在進行中呢? 來的查詢都給你返回 null , 等到 commit 提交時候在查詢就可以從緩存中取數據了。

public class TransactionalCache implements Cache {

  private static final Log log = LogFactory.getLog(TransactionalCache.class);
	// 真正的緩存
  private final Cache delegate;
  // 是否清理已經提交的實物
  private boolean clearOnCommit;
  // 可以理解爲暫存區
  private final Map<Object, Object> entriesToAddOnCommit;
  // 緩存中沒有的key
  private final Set<Object> entriesMissedInCache;

  public TransactionalCache(Cache delegate) {
    this.delegate = delegate;
    this.clearOnCommit = false;
    this.entriesToAddOnCommit = new HashMap<Object, Object>();
    this.entriesMissedInCache = new HashSet<Object>();
  }

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

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

  @Override
  public Object getObject(Object key) {
    // 先從緩存中拿數據
    Object object = delegate.getObject(key);
    if (object == null) {
      // 如果沒有添加到set集合中
      entriesMissedInCache.add(key);
    }
    // 返回數據庫的數據。
    if (clearOnCommit) {
      return null;
    } else {
      return object;
    }
  }

  @Override
  public ReadWriteLock getReadWriteLock() {
    return null;
  }

  @Override
  public void putObject(Object key, Object object) {
    entriesToAddOnCommit.put(key, object);
  }

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

  @Override
  public void clear() {
    clearOnCommit = true;
    entriesToAddOnCommit.clear();
  }

  public void commit() {
    if (clearOnCommit) {
      delegate.clear();
    }
    flushPendingEntries();
    reset();
  }

  public void rollback() {
    unlockMissedEntries();
    reset();
  }

  private void reset() {
    //1. 是否清除提交
    clearOnCommit = false;
    //2. 暫存區清理,代表這個事務從頭開始做了,之前的清理掉
    entriesToAddOnCommit.clear();
    //3. 同上
    entriesMissedInCache.clear();
  }
	
  /** 
   * 將暫存區的數據提交到緩存中
   **/
  private void flushPendingEntries() {
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
      delegate.putObject(entry.getKey(), entry.getValue());
    }
    //如果緩存中不包含這個key,就將key對應的value設置爲默認值null
    for (Object entry : entriesMissedInCache) {
      if (!entriesToAddOnCommit.containsKey(entry)) {
        delegate.putObject(entry, null);
      }
    }
  }

  // 移除缺失的key,就是這個緩存中沒有的key都移除掉
  private void unlockMissedEntries() {
    for (Object entry : entriesMissedInCache) {
      try {
        delegate.removeObject(entry);
      } catch (Exception e) {
        log.warn("Unexpected exception while notifiying a rollback to the cache adapter."
            + "Consider upgrading your cache adapter to the latest version.  Cause: " + e);
      }
    }
  }

}

10. WeakCache

從名字上看跟 SoftCache 有點關係,Soft引用是當內存不夠用時候才清理, 而Weak 弱引用則相反, 只要有GC就會回收。 所以他們的類型特性並不是自己實現的,而是依賴於 Reference<T> 類的特性,所以代碼就不看了基本和 SoftCache 實現一摸一樣。

感謝您的閱讀,本文由 程序猿升級課 版權所有。如若轉載,請註明出處:程序猿升級課(https://blog.springlearn.cn/)

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