关于java:彻底搞明白mybatis缓存上

36次阅读

共计 10366 个字符,预计需要花费 26 分钟才能阅读完成。

在 Web 利用中,缓存是必不可少的组件。通常咱们都会用 Redis 或 memcached 等缓存中间件,拦挡大量奔向数据库的申请,加重数据库压力。作为一个重要的组件,MyBatis 天然也在外部提供了相应的反对。通过在框架层面减少缓存性能,可减轻数据库的压力,同时又能够晋升查问速度,堪称两全其美。MyBatis 缓存构造由一级缓存和二级缓存形成,这两级缓存均是应用 Cache 接口的实现类。因而,在接下里的章节中,我将首先会向大家介绍 Cache 几种实现类的源码,而后再剖析一级和二级缓存的实现。

本文次要内容:

Mybatis 缓存体系结构

Mybatis 跟缓存相干的类都在 cache 包目录下,在后面的文章中咱们也提过,明天才来具体说说。其中有一个顶层接口 Cache,并且只有一个默认的实现类 PerpetualCache。

上面是 Cached 的类图:

既然 PerpetualCache 是默认实现类,那么咱们就从他下手。

PerpetualCache

PerpetualCache 这个对象会创立,所以这个叫做根底缓存。然而缓存又能够有很多额定的性能,比如说:回收策略、日志记录、定时刷新等等,如果需要的话,就能够在根底缓存上加上这些性能,如果不喜爱就不加。这里是不是想到了一种设计模式 —– 装璜器设计模式。PerpetualCache 相当于装璜模式中的 ConcreteComponent。

装璜器模式是指在不扭转原有对象的根底之上,将性能附加到对象上,提供了比继承更有弹性的替换计划,即扩大原有对象的性能。

除了缓存之外,Mybatis 也定义很多的装璜器,同样实现了 Cache 接口,通过这些装璜器能够额定实现很多性能。

这些缓存是怎么分类的呢?

所有的缓存能够大体归为三类:根本类缓存、淘汰算法缓存、装璜器缓存。

上面把每个缓存进行具体阐明和比照:

缓存实现类源码

PerpetualCache 源码

PerpetualCache 是一个具备基本功能的缓存类,外部应用了 HashMap 实现缓存性能。它的源码如下:

`public class PerpetualCache implements Cache {
    
      private final String id;
      // 应用 Map 作为缓存
      private Map<Object, Object> cache = new HashMap<>();
    
      public PerpetualCache(String id) {
        this.id = id;
      }
    
      @Override
      public String getId() {
        return id;
      }
    
      @Override
      public int getSize() {
        return cache.size();
      }
      // 存储键值对到 HashMap
      @Override
      public void putObject(Object key, Object value) {
        cache.put(key, value);
      }
      // 查找缓存项
      @Override
      public Object getObject(Object key) {
        return cache.get(key);
      }
      // 移除缓存项
      @Override
      public Object removeObject(Object key) {
        return cache.remove(key);
      }
      // 清空缓存
      @Override
      public void clear() {
        cache.clear();
      }
       // 局部代码省略
    }`

下面是 PerpetualCache 的全副代码,也就是所谓的根本缓存,很简略。接下来,咱们通过装璜类对该类进行装璜,使其性能变的丰盛起来。

LruCache

LruCache,顾名思义,是一种具备 LRU(Least recently used, 最近起码应用) 算法的缓存实现类。

除此之外,MyBatis 还提供了具备 FIFO 策略的缓存 FifoCache。不过并未提供 LFU (Least Frequently Used,最近起码应用算法) 缓存,也是一种常见的缓存算法,如果大家有趣味,能够自行拓展。

接下来,咱们来看一下 LruCache 的实现。

`public class LruCache implements Cache {
    
        private final Cache delegate;
        private Map<Object, Object> keyMap;
        private Object eldestKey;
    
        public LruCache(Cache delegate) {
            this.delegate = delegate;
            setSize(1024);
        }
        
        public int getSize() {
            return delegate.getSize();
        }
    
        public void setSize(final int size) {
            /*
             * 初始化 keyMap,留神,keyMap 的类型继承自 LinkedHashMap,
             * 并笼罩了 removeEldestEntry 办法
             */
            keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
                private static final long serialVersionUID = 4267176411845948333L;
    
                // 笼罩 LinkedHashMap 的 removeEldestEntry 办法
                @Override
                protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
                    boolean tooBig = size() > size;
                    if (tooBig) {
                        // 获取将要被移除缓存项的键值
                        eldestKey = eldest.getKey();
                    }
                    return tooBig;
                }
            };
        }
    
        @Override
        public void putObject(Object key, Object value) {
            // 存储缓存项
            delegate.putObject(key, value);
            cycleKeyList(key);
        }
    
        @Override
        public Object getObject(Object key) {
            // 刷新 key 在 keyMap 中的地位
            keyMap.get(key);
            // 从被装璜类中获取相应缓存项
            return delegate.getObject(key);
        }
    
        @Override
        public Object removeObject(Object key) {
            // 从被装璜类中移除相应的缓存项
            return delegate.removeObject(key);
        }
        // 清空缓存
        @Override
        public void clear() {
            delegate.clear();
            keyMap.clear();
        }
    
        private void cycleKeyList(Object key) {
            // 存储 key 到 keyMap 中
            keyMap.put(key, key);
            if (eldestKey != null) {
                // 从被装璜类中移除相应的缓存项
                delegate.removeObject(eldestKey);
                eldestKey = null;
            }
        }
        // 省略局部代码
    }`

从下面代码中能够看出,LruCache 的 keyMap 属性是实现 LRU 策略的要害,该属性类型继承自 LinkedHashMap,并笼罩了 removeEldestEntry 办法。LinkedHashMap 可放弃键值对的插入程序,当插入一个新的键值对时,

LinkedHashMap 外部的 tail 节点会指向最新插入的节点。head 节点则指向第一个被插入的键值对,也就是最久未被拜访的那个键值对。默认状况下,LinkedHashMap 仅保护键值对的插入程序。若要基于 LinkedHashMap 实现 LRU 缓存,还需通过构造方法将 LinkedHashMap 的 accessOrder 属性设为 true,此时 LinkedHashMap 会保护键值对的拜访程序。

比方,下面代码中 getObject 办法中执行了这样一句代码 keyMap.get(key),目标是刷新 key 对应的键值对在 LinkedHashMap 的地位。LinkedHashMap 会将 key 对应的键值对挪动到链表的尾部,尾部节点示意最久刚被拜访过或者插入的节点。除了需将 accessOrder 设为 true,还需笼罩 removeEldestEntry 办法。LinkedHashMap 在插入新的键值对时会调用该办法,以决定是否在插入新的键值对后,移除老的键值对。

在下面的代码中,当被装璜类的容量超出了 keyMap 的所规定的容量(由构造方法传入)后,keyMap 会移除最长工夫未被拜访的键,并保留到 eldestKey 中,而后由 cycleKeyList 办法将 eldestKey 传给被装璜类的 removeObject 办法,移除相应的缓存我的项目。

BlockingCache

BlockingCache 实现了阻塞个性,该个性是基于 Java 重入锁实现的。同一时刻下,BlockingCache 仅容许一个线程拜访指定 key 的缓存项,其余线程将会被阻塞住。

上面咱们来看一下 BlockingCache 的源码。

`public class BlockingCache implements Cache {
    
        private long timeout;
        private final Cache delegate;
        private final ConcurrentHashMap<Object, ReentrantLock> locks;
    
        public BlockingCache(Cache delegate) {
            this.delegate = delegate;
            this.locks = new ConcurrentHashMap<Object, ReentrantLock>();
        }
    
        @Override
        public void putObject(Object key, Object value) {
            try {
                // 存储缓存项
                delegate.putObject(key, value);
            } finally {
                // 开释锁
                releaseLock(key);
            }
        }
    
        @Override
        public Object getObject(Object key) {
            // 请        // 申请锁
            acquireLock(key);
            Object value = delegate.getObject(key);
            // 若缓存命中,则开释锁。须要留神的是,未命中则不开释锁
            if (value != null) {
                // 开释锁
                releaseLock(key);
            }
            return value;
        }
    
        @Override
        public Object removeObject(Object key) {
            // 开释锁
            releaseLock(key);
            return null;
        }
    
        private ReentrantLock getLockForKey(Object key) {
            ReentrantLock lock = new ReentrantLock();
            // 存储 <key, Lock> 键值对到 locks 中
            ReentrantLock previous = locks.putIfAbsent(key, lock);
            return previous == null ? lock : previous;
        }
    
        private void acquireLock(Object key) {
            Lock lock = getLockForKey(key);
            if (timeout > 0) {
                try {
                    // 尝试加锁
                    boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS);
                    if (!acquired) {
                        throw new CacheException(“…”);
                    }
                } catch (InterruptedException e) {
                    throw new CacheException(“…”);
                }
            } else {
                // 加锁
                lock.lock();
            }
        }
    
        private void releaseLock(Object key) {
            // 获取与以后 key 对应的锁
            ReentrantLock lock = locks.get(key);
            if (lock.isHeldByCurrentThread()) {
                // 开释锁
                lock.unlock();
            }
        }
        
        // 省略局部代码
    }`

如上,查问缓存时,getObject 办法会先获取与 key 对应的锁,并加锁。若缓存命中,getObject 办法会开释锁,否则将始终锁定。getObject 办法若返回 null,示意缓存未命中。此时 MyBatis 会进行数据库查问,并调用 putObject 办法存储查问后果。同时,putObject 办法会将指定 key 对应的锁进行解锁,这样被阻塞的线程即可复原运行。

下面的形容有点啰嗦,倒是 BlockingCache 类的正文说到比拟简单明了。这里援用一下:

`It sets a lock over a cache key when the element is not found in cache.
This way, other threads will wait until this element is filled instead of hitting the database.
`

这段话的意思是,当指定 key 对应元素不存在于缓存中时,BlockingCache 会依据 lock 进行加锁。此时,其余线程将会进入期待状态,直到与 key 对应的元素被填充到缓存中。而不是让所有线程都去拜访数据库。

在下面代码中,removeObject 办法的逻辑很奇怪,仅调用了 releaseLock 办法开释锁,却没有调用被装璜类的 removeObject 办法移除指定缓存项。这样做是为什么呢?大家能够先思考,答案将在剖析二级缓存的相干逻辑时剖析。

CacheKey

在 MyBatis 中,引入缓存的目标是为进步查问效率,升高数据库压力。既然 MyBatis 引入了缓存,那么大家思考过缓存中的 key 和 value 的值别离是什么吗?大家可能很容易能答复出 value 的内容,不就是 SQL 的查问后果吗。

那 key 是什么呢?是字符串,还是其余什么对象?如果是字符串的话,那么大家首先能想到的是用 SQL 语句作为 key。但这是不对的.

比方:

`SELECT * FROM author where id > ?
`

d > 1 和 id > 10 查出来的后果可能是不同的,所以咱们不能简略的应用 SQL 语句作为 key。从这里能够看进去,运行时参数将会影响查问后果,因而咱们的 key 应该涵盖运行时参数。除此之外呢,如果进行分页查问也会导致查问后果不同,因而 key 也应该涵盖分页参数。综上,咱们不能应用简略的 SQL 语句作为 key。应该思考应用一种复合对象,能涵盖可影响查问后果的因子。在 MyBatis 中,这种复合对象就是 CacheKey。

上面来看一下它的定义。

`public class CacheKey implements Cloneable, Serializable {
    private static final int DEFAULT_MULTIPLYER = 37;
    private static final int DEFAULT_HASHCODE = 17;
    // 乘子,默认为 37
    private final int multiplier;
    // CacheKey 的 hashCode,综合了各种影响因子
    private int hashcode;
    // 校验和
    private long checksum;
    // 影响因子个数
    private int count;
    // 影响因子汇合
    private List<Object> updateList;
    public CacheKey() {
        this.hashcode = DEFAULT_HASHCODE;
        this.multiplier = DEFAULT_MULTIPLYER;
        this.count = 0;
        this.updateList = new ArrayList<Object>();
    }
        // 省略其余办法
}
`

如上,除了 multiplier 是恒定不变的,其余变量将在更新操作中被批改。

上面看一下更新操作的代码。

`/* 每当执行更新操作时,示意有新的影响因子参加计算 /
    public void update(Object object) {
            int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
        // 自增 count
        count++;
        // 计算校验和
        checksum += baseHashCode;
        // 更新 baseHashCode
        baseHashCode *= count;
    
        // 计算 hashCode
        hashcode = multiplier * hashcode + baseHashCode;
    
        // 保留影响因子
        updateList.add(object);
    }`

当一直有新的影响因子参加计算时,hashcode 和 checksum 将会变得愈发简单和随机。这样可升高抵触率,使 CacheKey 可在缓存中更平均的散布。CacheKey 最终要作为键存入 HashMap,因而它须要笼罩 equals 和 hashCode 办法。

上面咱们来看一下这两个办法的实现。

`public boolean equals(Object object) {
    // 检测是否为同一个对象
    if (this == object) {
        return true;
    }
    // 检测 object 是否为 CacheKey
    if (!(object instanceof CacheKey)) {
        return false;
    }
   final CacheKey cacheKey = (CacheKey) object;
    
    // 检测 hashCode 是否相等
    if (hashcode != cacheKey.hashcode) {
        return false;
    }
    // 检测校验和是否雷同
    if (checksum != cacheKey.checksum) {
        return false;
    }
    // 检测 coutn 是否雷同
    if (count != cacheKey.count) {
        return false;
    }
    // 如果下面的检测都通过了,上面别离对每个影响因子进行比拟
    for (int i = 0; i < updateList.size(); i++) {
        Object thisObject = updateList.get(i);
        Object thatObject = cacheKey.updateList.get(i);
        if (!ArrayUtil.equals(thisObject, thatObject)) {
            return false;
        }
    }
    return true;
}
    
public int hashCode() {
    // 返回 hashcode 变量
    return hashcode;
}
`

equals 办法的检测逻辑比拟严格,对 CacheKey 中多个成员变量进行了检测,已保障两者相等。hashCode 办法比较简单,返回 hashcode 变量即可。

对于 CacheKey 就先剖析到这,CacheKey 在一二级缓存中会被用到,接下来还会看到它的身影。

好吧,终于把源码缓存实现类的源码拔完了。

正文完
 0