上一章分析了 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 就好多了.
来源: https://www.cnblogs.com/xiaobingblog/p/13393744.html