myBatis源码解析-缓存篇(2)
上一章分析了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功能了。了解基本的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常用的缓存装饰类。下文进行分析。