myBatis原始碼解析-快取篇(2)

超人小冰發表於2020-07-28

上一章分析了mybatis的原始碼的日誌模組,像我們經常說的mybatis一級快取,二級快取,快取究竟在底層是怎樣實現的。此次開始分析快取模組

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

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提供了日誌列印功能。使用者想讓自己的快取具備什麼功能,就使用這些裝飾者類進行裝飾。

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加鎖,保證執行緒安全。

4. 分析快取裝飾類LruCache

介紹LruCache前,先介紹下Lru的實現,Lru是很常用的淘汰策略,意為最近最少使用的物件。檢視LruCache,發現內部使用了LinkedHashMap,熟悉LinkedHashMap的夥伴應該知道了。我們一般手寫LRU功能就是通過複寫LinkedHashMap的方法來實現,LruCache也一樣。先大致瞭解下LinkedHashMap。

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

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

// 內部維護了一個自定義的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功能了。

下文分析LruCache就好多了。

 

相關文章