myBatis源碼解析-緩存篇(2)

上一章分析了mybatis的源碼的日誌模塊,像咱們常常說的mybatis一級緩存,二級緩存,緩存究竟在底層是怎樣實現的。這次開始分析緩存模塊node

1. 源碼位置,mybatis源碼包位於org.apache.ibatis.cache下,如圖apache

2. 先從org.apache.ibatis.cache下的cache接口開始緩存

// 緩存接口
public interface Cache {
  // 獲取緩存ID
  String getId();
  // 放入緩存
  void putObject(Object key, Object value);
  // 獲取緩存
  Object getObject(Object key);
  // 移除某一緩存
  Object removeObject(Object key);
  // 清除緩存
  void clear();
  // 獲取緩存大小
  int getSize();
  // 獲取鎖
  ReadWriteLock getReadWriteLock();
}

mybatis提供了自定義的緩存接口,功能通俗易懂,沒什麼好解釋的。有接口,必然有實現,看一下緩存接口的基本實現類PerpetualCache,所在路徑爲org.apache.ibatis.cache.impl下。安全

public class PerpetualCache implements Cache {

  // 緩存的ID
  private String id;
  // 使用HashMap充當緩存(老套路,緩存底層實現基本都是map)
  private Map<Object, Object> cache = new HashMap<Object, Object>();
  // 惟一構造方法(即緩存必須有ID)
  public PerpetualCache(String id) {
    this.id = id;
  }
  // 獲取緩存的惟一ID
  public String getId() {
    return id;
  }
  // 獲取緩存的大小,實際就是hashmap的大小
  public int getSize() {
    return cache.size();
  }
  // 放入緩存,實際就是放入hashmap
  public void putObject(Object key, Object value) {
    cache.put(key, value);
  }
  // 從緩存獲取,實際就是從hashmap中獲取
  public Object getObject(Object key) {
    return cache.get(key);
  }
  // 從緩存移除
  public Object removeObject(Object key) {
    return cache.remove(key);
  }
  // hashmap清除數據方法
  public void clear() {
    cache.clear();
  }
  // 暫時沒有其實現
  public ReadWriteLock getReadWriteLock() {
    return null;
  }
  // 緩存是否相同
  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類,直接返回false

    Cache otherCache = (Cache) o; // 強制轉換爲cache
    return getId().equals(otherCache.getId()); // 直接比較ID是否相等
  }
  // 獲取hashCode
  public int hashCode() {
    if (getId() == null) throw new CacheException("Cache instances require an ID.");
    return getId().hashCode();
  }

}
    

如上分析,mybatis的基本緩存實現類其實就是內部維護了一個HashMap,經過對HashMap操做來實現基本的功能。但須要注意的是,判斷兩個緩存是否相等,是比較的緩存ID是否相等。看Cache otherCache = (Cache) o;也就是說緩存接口可能有多種實現,也確實如此。PerpetualCache只提供了緩存的基本實現功能,但一看HashMap就是不安全的類,多線程下確定會出問題。又好比說我想這個緩存有固定大小,緩存過時策越爲先進先出或者LRU功能等。myabtis確定想到這點,查看org.apache.ibatis.cache.decorators包。看名字就知道用到了裝飾者模式。查看包下的類,如SynchronizedCache爲緩存保障了線程安全,LruCache定義了緩存的過時策略爲淘汰最近最少訪問的數據,LoggIngCache提供了日誌打印功能。用戶想讓本身的緩存具有什麼功能,就使用這些裝飾者類進行裝飾。mybatis

3. 分析緩存裝飾類SynchronizedCache多線程

// 在操做前加鎖,保證線程安全
  @Override
  public synchronized int getSize() {
    return delegate.getSize();
  }

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

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

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

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

很簡單。就是在方法前使用synchronized加鎖,保證線程安全。app

4. 分析緩存裝飾類LruCacheide

介紹LruCache前,先介紹下Lru的實現,Lru是很經常使用的淘汰策略,意爲最近最少使用的對象。查看LruCache,發現內部使用了LinkedHashMap,熟悉LinkedHashMap的夥伴應該知道了。咱們通常手寫LRU功能就是經過複寫LinkedHashMap的方法來實現,LruCache也同樣。先大體瞭解下LinkedHashMap。ui

public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>

LinkedHashMap繼承HashMap類,實際上就是對HashMap的一個封裝。this

// 內部維護了一個自定義的Entry,集成HashMap中的node類
static class Entry<K,V> extends HashMap.Node<K,V> {
        // linkedHashmap用來鏈接節點的字段,根據這兩個字段可查找按順序插入的節點
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }

查看LinkedHashMap構造方法,具體訪問順序見下文分析

    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        // 調用HashMap的構造方法
        super(initialCapacity, loadFactor);
        // 訪問順序維護,默認false不開啓
        this.accessOrder = accessOrder;
    }    

引入兩張圖來理解下HashMap和LinkedHashMap

 

 以上時HashMap的結構,採用拉鍊法解決衝突。LinkedHashMap在HashMap基礎上增長了一個雙向鏈表來表示節點插入順序。

 

 如上,節點上多出的紅色和藍色箭頭表明了Entry中的before和after。在put元素時,會自動在尾節點後加上該元素,維持雙向鏈表。瞭解LinkedHashMap結構後,在看看究竟什麼是維護節點的訪問順序。先說結論,當開啓accessOrder後,在對元素進行get操做時,會將該元素放在雙向鏈表的隊尾節點。源碼以下:

 public V get(Object key) {
        Node<K,V> e;
       // 調用HashMap的getNode方法,獲取元素
        if ((e = getNode(hash(key), key)) == null)
            return null;
       // 默認爲false,若是開啓維護鏈表訪問順序,執行以下方法
        if (accessOrder)
            afterNodeAccess(e);
        return e.value;
    }


// 方法實現(將e放入尾節點處)
void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;
        // 當節點不是雙向鏈表的尾節點時
        if (accessOrder && (last = tail) != e) {
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after; // 將待調整的e節點賦值給p
           
            p.after = null;
            if (b == null) // 說明e爲頭節點,將老e的下一節點值爲頭節點
                head = a;
            else
                b.after = a;// 不然,e的上一節點直接指向e的下一節點
            if (a != null)
                a.before = b; // e的下一節點的上節點爲e的上一節點
            else
                last = b;  
            if (last == null)
                head = p;  
            else {
                p.before = last;   // last和p互相鏈接
                last.after = p;
            }
            tail = p;   // 將雙向鏈表的尾節點指向p
            ++modCount; // 修改次數加以
        }
    }

代碼很簡單,如上面的圖,我訪問了節點值爲3的節點,那木通過get操做後,結構變成以下

 

 

通過如上分析咱們知道,若是限制雙向鏈表的長度,每次刪除頭節點的值,就變爲一個lru的淘汰策略了。舉個例子,我想限制雙向鏈表的長度爲3,依次put 1 2 3,鏈表爲 1 -> 2 -> 3,訪問元素2,鏈表變爲 1 -> 3-> 2,而後put 4 ,發現鏈表長度超過3了,淘汰1,鏈表變爲3 -> 2 ->4;

那木linkedHashMap是怎樣知道自定義的限制策略,看代碼,由於LinkedHashMap中沒有提供本身的put方法,是直接調用的HashMap的put方法,查看hashMap代碼以下:

// hashMap
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        // 看這個方法
        afterNodeInsertion(evict);
        return null;
    }

// linkedHashMap重寫了此方法

 void afterNodeInsertion(boolean evict) { // possibly remove eldest
        LinkedHashMap.Entry<K,V> first;
        // removeEldestEntry默認返回fasle
        if (evict && (first = head) != null && removeEldestEntry(first)) {
            K key = first.key;
            // 移除雙向鏈表中的頭指針元素
            removeNode(hash(key), key, null, false, true);
        }
    }

原來只須要從新實現removeEldestEntry就能夠自定義實現lru功能了。瞭解基本的lru原理後,開始分析LruCache。

public class LruCache implements Cache {
  // 被裝飾的緩存類,即真實的緩存類,提供真正的緩存能力
  private final Cache delegate;
  // 內部維護的一個linkedHashMap,用來實現LRU功能
  private Map<Object, Object> keyMap;
  // 待淘汰的緩存元素
  private Object eldestKey;
  // 惟一構造方法
  public LruCache(Cache delegate) {
    this.delegate = delegate; // 被裝飾的緩存類
    setSize(1024); // 設置緩存大小
  }
  ....
 }

經分析,LruCache仍是個裝飾類。內部除了維護真正的Cache外,還維護了一個LinkedHashMap,用來實現Lru功能,查看其構造方法。

// 惟一構造方法
  public LruCache(Cache delegate) {
    this.delegate = delegate; // 被裝飾的緩存類
    setSize(1024); // 設置緩存大小
  }
  
   // setSize()是構造方法中方法
  public void setSize(final int size) {
    // 初始化keyMap
    keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
      private static final long serialVersionUID = 4267176411845948333L;
      // 何時自動刪除緩存元素,此處是根據當緩存數量超過指定的數量,在LinkedHashMap內部刪除元素
      protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
        boolean tooBig = size() > size;
        if (tooBig) {
          // 將待刪除元素賦值給eldestKey,後續會根據此值是否爲空在真實緩存中刪除
          eldestKey = eldest.getKey();
        }
        return tooBig;
      }
    };
  }

和上文分析同樣,重寫了removeEldestEntry方法。此方法返回一個boolean值,當緩存的大小超過自定義大小,返回true,此時linkedHashMap中會自動刪除eldest元素。在真實緩存cache中也將此元素刪除。保持真實cache和linkedHashMap元素一致。其實就是用linkedHashMap的lru特性來保證cache也具備此lru特性。

分析put方法和get方法驗證此結論

@Override
  public Object getObject(Object key) {
    keyMap.get(key); // 觸發linkedHashMap中get方法,將key對應的元素放入隊尾
    return delegate.getObject(key); // 調用真實的緩存get方法
  }
  
  // 放入緩存時,除了在真實緩存中放一份外,還會在LinkedHashMap中放一份
   @Override
  public void putObject(Object key, Object value) {
    delegate.putObject(key, value);
    // 調用LinkedHashMap的方法
    cycleKeyList(key);
  }
  
  private void cycleKeyList(Object key) {
    // linkedHashMap中put,會觸發removeEldestEntry方法,若是緩存大小超過指定大小,則將雙向鏈表對頭值賦值給eldestKey
    keyMap.put(key, key); 
    // 檢查eldestKey是否爲空。不爲空,則表明此元素是淘汰的元素了,須要在真實緩存中刪除。
    if (eldestKey != null) {
      // 真實緩存中刪除
      delegate.removeObject(eldestKey);
      eldestKey = null;
    }
  }

Lru分析結束,除了LruCache外,TransactionCache也是mybatis經常使用的緩存裝飾類。下文進行分析。

相關文章
相關標籤/搜索