上一章分析了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經常使用的緩存裝飾類。下文進行分析。