12.23 看Mybatis如何花樣設計 Cache

看Mybatis如何花樣設計 Cache

看Mybatis如何花樣設計 Cache

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

看Mybatis如何花樣設計 Cache

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

一、模式分析

看Mybatis如何花樣設計 Cache

從目錄就很清晰看出,核心就是 impl 包下面只有一個,其他都是裝飾器模式,在 decorators 包下

1. Cache

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

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

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

2. PerpetualCache

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

<code>public class PerpetualCache implements Cache {  // 唯一標識  private final String id;  // 就是一個HashMap結構  private Map<object> cache = new HashMap<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();  }}/<object>/<object>/<code>

3. 小總結

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

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

二、開始重頭戲

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

看Mybatis如何花樣設計 Cache

1. BlockingCache

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

<code>public class BlockingCache implements Cache {  private long timeout;  //實現Cache接口的緩存對象  private final Cache delegate;  //對每個key生成一個鎖對象  private final ConcurrentHashMap<object> locks;  public BlockingCache(Cache delegate) {    this.delegate = delegate;    this.locks = new ConcurrentHashMap<object>();  }  @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;  }  }/<object>/<object>/<code> 

建議看代碼註釋

方法解釋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 來實現先進先出。

<code>  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);    }  }/<object>/<object>/<code>

3. LoggingCache

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

<code>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;  }/<code>

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

<code>public class LoggingCache implements Cache {  private final Log log;  private final Cache delegate;  //1. 總請求次數  protected int requests = 0;  //2. 命中次數  protected int hits = 0;  ...}  /<code>

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

<code>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;  }/<code>

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

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

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

4. LruCache

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

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

<code>public V get(Object key) {        Node e;        if ((e = getNode(hash(key), key)) == null)            return null;        //1. 判斷是否開始LRU策略        if (accessOrder)            //2. 開啟就往後面放            afterNodeAccess(e);        return e.value;    }/<code>
看Mybatis如何花樣設計 Cache

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

看Mybatis如何花樣設計 Cache

5. ScheduledCache

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

<code>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;  }}/<code>

核心代碼

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

6. SerializedCache

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

為啥要支持序列化?

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

如何解決這種問題

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

只看一下核心代碼

  1. putObject 將對象序列化成 byte[]
  2. getObject 將 byte[]反序列化成對象
<code>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;  }/<code>

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

Java開發之深淺拷貝

7. SoftCache

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

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

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

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

<code>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>();  }}  /<object>/<object>/<object>/<object>/<code>

hardLinksToAvoidGarbageCollection 硬連接,避免垃圾收集 queueOfGarbageCollectedEntries 垃圾要收集的隊列 numberOfHardLinks 硬連接數量

<code>@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);    }  }/<object>/<object>/<code>

8. SynchronizedCache

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

看Mybatis如何花樣設計 Cache

9. TransactionalCache

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

看Mybatis如何花樣設計 Cache

所有的查詢都先從一級緩存中查詢

看Mybatis如何花樣設計 Cache

看Mybatis如何花樣設計 Cache

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

看Mybatis如何花樣設計 Cache

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

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

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

<code>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> entriesToAddOnCommit;  // 緩存中沒有的key  private final Set<object> entriesMissedInCache;  public TransactionalCache(Cache delegate) {    this.delegate = delegate;    this.clearOnCommit = false;    this.entriesToAddOnCommit = new HashMap<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> 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);      }    }  }}/<object>/<object>/<object>/<object>/<object>/<code>

10. WeakCache

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


程序猿升級課

看Mybatis如何花樣設計 Cache

看Mybatis如何花樣設計 Cache


分享到:


相關文章: