MyBatis 作为一个强大的持久层框架,缓存是其必不可少的功能之一。 MyBatis 中的缓存是两层结构的,分为一级缓存、二级缓存,但在本质上是相同的,它们使用的都是 Cache 接 口的实现。
缓存模式主要是设计模式是装饰器模式。
在 MyBatis 的缓存模块中,使用了装饰器模式的变体,其中将 Decorator 接口和 Component接口合并为一个 Component 接口,得到的类问结构下图所示。
使用装饰器模式的有两个明显的优点:
1、相较于继承来说,装饰器模式的灵活性更强,可扩展性也强。正如前面所说,继承方 式会导致大量子类的情况。而装饰者模式可以将复杂的功能切分成一个个独立的装饰 器,通过多个独立装饰器的动态组合,创建不同功能的组件,从而满足多种不同需求。
2、当有新功能需要添加时,只需要添加新的装饰器实现类,然后通过组合方式添加这个 新装饰器即可,无须修改己有类的代码,符合“开放一封闭”原则。
但是,随着添加的新需求越来越多,可能会创建出嵌套多层装饰器的对象,这增加了系统 的复杂性, 也增加了理解的难度和定位错误的难度。
一、Cache 接口及其实现
MyBatis 中缓存模块相关的代码位于 cache 包下, 其中 Cache 接口是缓存模块中最核心的接 口,它定义了所有缓存的基本行为。 Cache 接口的定义如下:
public interface Cache {
//该缓存对象的id
String getId();
//向缓存中添加数据,一般情况下, key 是 CacheKey, value 是查询结果
void putObject(Object key, Object value);
//根据指定的 key,在缓存中查找对应的结果对象
Object getObject(Object key);
//删除 key 对应的缓存项
Object removeObject(Object key);
//清空缓存
void clear();
//缓存项的个数,该方法不会被 MyBatis 核心代码使用,所以可提供空实现
int getSize();
//获取读写锁,该方法不会被 MyBatis 核心代码使用,所以可提供空实现
default ReadWriteLock getReadWriteLock() {
return null;
}
}
Cache 接口的实现类有多个,但大部分都是装饰器,只有 PerpetualCache 提供了 Cache 接口的基本实现。
1.1 Perpetual Cache
Perpetual Cache 在缓存模块中扮演着 ConcreteComponent 的角色,其实现比较简单,底层使 用 HashMap 记录缓存项,也是通过该 HashMap 对象的方法实现的 Cache 接口中定义的相应方 法。 PerpetualCache 的具体实现如下:
public class PerpetualCache implements Cache {
//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();}
@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();}
// ... 重写 了 equals ()方法和 hashCode ()方法,两者都只关心 id 字段,并不关心 cache 字段(略)
}
下面来介绍 cache.decorators 包下提供的装饰器,它们都直接实现了 Cache 接口,扮演着 ConcreteDecorator 的角色。这些装饰器会在 PerpetualCache 的基础上提供一些额外的功能,通 过多个组合后满足一个特定的需求,后面介绍二级缓存时,会见到这些装饰器是如何完成动态 组合的。
1.2 BlockingCache
BlockingCache 是阻塞版本的缓存装饰器,它会保证只有一个线程到数据库中查找指定 key 对应的数据。属于细粒度锁,会阻塞相同key的线程,对于不同key的线程不会阻塞。
BlockingCache 中各个字段的含义如下:
private long timeout;//阻塞的超时时长
private final Cache delegate;//被装饰的底层对象,一般是PerpetualCache
//每个 key 都有对应的 ReentrantLock 对象
private final ConcurrentHashMap<Object, ReentrantLock> locks;//锁对象集,粒度到key值
public BlockingCache(Cache delegate) {
this.delegate = delegate;
this.locks = new ConcurrentHashMap<>();
}
假设线程 A 在 BlockingCache 中未查找到 keyA 对应的缓存项时,线程 A 会获取 keyA 对应 的锁,这样后续线程在查找 keyA 时会发生阻塞,如下图所示:
BlockingCache.getObject()方法的代码如下:
/**
* 细粒度锁
* 在get的时候先去获得key的锁
* @param key The key
* @return
*/
@Override
public Object getObject(Object key) {
//尝试去获取锁
acquireLock(key);
Object value = delegate.getObject(key);
//缓存有 key 对应的缓存项,择放锁,否则继续持有锁
if (value != null) {
//释放锁
releaseLock(key);
}
return value;
}
acquireLock(key)会去尝试获取当前key的锁,如果当前key没有对象锁则为其创建新的ReentrantLock对象,在对其加锁。如果有改key的对象锁存在则使用该对象,对其加锁。如果加锁失败,阻塞一段时间。其代码如下:
//acquireLock()方法中会尝试获取指定 key 对应的锁。
// 如果该 key 没有对应的锁对象则为其创建新的 ReentrantLock 对象,再加锁;如果获取锁失败, 则阻塞一段时间。
private void acquireLock(Object key) {
//获取ReentrantLock 对象
Lock lock = getLockForKey(key);
if (timeout > 0) {//使用带超时时间的锁
try {
boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS);
if (!acquired) {//如果超时抛出异常
throw new CacheException("Couldn't get a lock in " + timeout + " for the key " + key + " at the cache " + delegate.getId());
}
} catch (InterruptedException e) {
throw new CacheException("Got interrupted while trying to acquire lock for key " + key, e);
}
} else {
lock.lock();
}
}
private ReentrantLock getLockForKey(Object key) {
//把新锁添加到locks集合中,如果添加成功使用新锁,如果添加失败则使用locks集合中的锁
return locks.computeIfAbsent(key, k -> new ReentrantLock());
}
假设线程 A 从数据库中查找到 keyA 对应的结果对象后,将结果对象放入到 BlockingCache 中,此时线程 A 会释放 keyA 对应的锁,唤醒阻塞在该锁上的线程。其他线程即可从 BlockingCache 中获取 keyA 对应的数据,而不是再次访问数据库
BlockingCache.putObject()方法的实现如下:
@Override
public void putObject(Object key, Object value) {
try {
delegate.putObject(key, value);
} finally {
releaseLock(key);
}
}
//释放锁
private void releaseLock(Object key) {
ReentrantLock lock = locks.get(key);
//锁是否被当前线程持有
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
1.3 FifoCache&LruCache
在很多场景中,为了控制缓存大小,系统需要按照一定的规则清楚缓存。fifocache 是先进先出版本的装饰器,当向缓存添加数据时,如果缓存数据个数已经达到上线,则清除最先进来的缓存数据。
FifoCache中属性如下所示:
private final Cache delegate;//底层被装饰的cache对象
//用于记录key的进入顺序,使用的是LinkedList类型的集合对象
private final Deque<Object> keyList;
private int size;//记录缓存项的上线,超过该项则清楚最老的数据默认是1024
public FifoCache(Cache delegate) {
this.delegate = delegate;
this.keyList = new LinkedList<>();
this.size = 1024;
}
FifoCacbe.getObject()和 removeObject()方法的实现都是直接调用底层 Cache 对象的对应方 法, 不再赘述。 在 FifoCacbe.p旧Object()方法中会完成缓存项个数的检测以及缓存的清理操作, 具体实现如下:
@Override
public void putObject(Object key, Object value) {
cycleKeyList(key);//把key放入队列中,检测并清理缓存
delegate.putObject(key, value);//记录缓存
}
private void cycleKeyList(Object key) {
keyList.addLast(key);//记录key
if (keyList.size() > size) {//如果缓存达到上线则清除最早记录
Object oldestKey = keyList.removeFirst();
delegate.removeObject(oldestKey);
}
}
LruCache 是按照近期最少使用算法(Least Recently Used, LRU)进行缓存清理的装饰器, 在需要清理缓存时,它会清除最近最少使用的缓存工页。LruCache 中定义的各个字段的含义如下:
private final Cache delegate;//被装饰的底层cache对象
//LinkedHashMap<Obj ect, Object>类型对象,它是一个有序的 HashMap,用于记录 key 最近的使用情况
private Map<Object, Object> keyMap;
private Object eldestKey;//记录最少被使用的缓存项的key
public LruCache(Cache delegate) {
this.delegate = delegate;
setSize(1024);
}
LruCache 的构造函数中默认设置的缓存大小是 1024,我们可以通过其 setSize()方法重新设 置缓存大小, 具体实现如下:
//重新设置缓存大小时,会重置 keyMap 字段
public void setSize(final int size) {
//LinkedHashMap的第三个参数为true,true表示表示该 LinkedHashMap 记录的顺序是
// access-order,也就是说 LinkedHashMap.get()方法会改变其记录的顺序
keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
private static final long serialVersionUID = 4267176411845948333L;
//重写方法 当调用 LinkedHashMap . put ()方法时,会调用该方法
@Override
protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
boolean tooBig = size() > size;
if (tooBig) {//如果已到达缓存上限,则更新 eldestKey 字段, 后面会删除该项
eldestKey = eldest.getKey();
}
return tooBig;
}
};
}
为了让读者更好地理解 LinkedHashMap,下图 展示了其原理,图中的虚线形成了一个队 列,当 LinkedHashMap.get()方法访问 K1时,会修改这条虚线队列将 K1项移动到队列尾部, LruCache 就是通过 LinkedHashMap 的这种特性来确定最久未使用的缓存项。
LruCache.getObject()方法除了返回缓存项,还会调用 keyMap.get()方法修改 key 的顺序,表 示指定的 key 最近被使用。 LruCache.putObject()方法除了添加缓存项,还会将 eldestKey 宇段指 定的缓存项清除掉。具体实现如下:
@Override
public Object getObject(Object key) {
keyMap.get(key); //修改key中在记录中的顺序
return delegate.getObject(key);
}
@Override
public void putObject(Object key, Object value) {
delegate.putObject(key, value);//加入缓存
cycleKeyList(key);//把key加入记录中并会清除缓存
}
private void cycleKeyList(Object key) {
//
keyMap.put(key, key);
if (eldestKey != null) {//eldestKey 不为空,表示已经达到缓存上
delegate.removeObject(eldestKey);//清除最少使用的缓存
eldestKey = null;
}
}
1.4 SoftCache&WeakCache
Java 提供的 4 种引 用类型,它们分别是强引用 (Strong Reference)、软引用 ( soft Reference)、弱引用(Weak Reference) 和幽灵引用(Phantom Reference)。详情可以自行了解。
SoftCache的实现。SoftCache中各个字段的 含义如下:
// 在 SoftCache 中,最近使用的一部分缓存项不会被 GC 回收,
// 这就是通过将其 value 添加到 II hardLinksToAvoidGarbageCollection
// 集合中实现的(即有强引用指向其 value) II hardLinksToAvoidGarbageCollection 集合是 LinkedList<Object>类型
private final Deque<Object> hardLinksToAvoidGarbageCollection;
//ReferenceQueue,引用队列,用于记录已经被GC回收的缓存项所对应的SoftEntry对象
private final ReferenceQueue<Object> queueOfGarbageCollectedEntries;
private final Cache delegate;
private int numberOfHardLinks;//强连接的个数, 默认值是 256
public SoftCache(Cache delegate) {
this.delegate = delegate;
this.numberOfHardLinks = 256;
this.hardLinksToAvoidGarbageCollection = new LinkedList<>();
this.queueOfGarbageCollectedEntries = new ReferenceQueue<>();
}
SoftCache 中缓存项的 value 是 SoftEntry对象, SoftEntry 继承了 SoftReference<Object>, 其中指向 key 的引用是强引用, 而指向 value 的引用是软引用 。 SoftEntry对象的实现如下:
private static class SoftEntry extends SoftReference<Object> {
private final Object key;
//
SoftEntry(Object key, Object value, ReferenceQueue<Object> garbageCollectionQueue) {
super(value, garbageCollectionQueue);//指向 value 的引用是软引用,且关联了引用队列
this.key = key;///key是强引用
}
}
SoftCache.putObject()方法除了向缓存中添加缓存项,还会清除己经被 GC 回收的缓存项, 其具体实现如下:
@Override
public void putObject(Object key, Object value) {
//清除已经被 GC 回收的缓存项
removeGarbageCollectedItems();
//向缓存中添加缓存项
delegate.putObject(key, new SoftEntry(key, value, queueOfGarbageCollectedEntries));
}
//下面是 removeGarbageCollecteditems()方法的实现:
private void removeGarbageCollectedItems() {
SoftEntry sv;
//遍历queueOfGarbageCollectedEntries 集合
while ((sv = (SoftEntry) queueOfGarbageCollectedEntries.poll()) != null) {
delegate.removeObject(sv.key);//将已经被 GC 回收的 value 对象对应的缓存项清除
}
}
SoftCache.getObject()方法除了从缓存中查找对应的 value,处理被 GC 回收的 value 对应的 缓存项, 还会更新 hardLinksToAvoidGarbageCollection 集合, 具体实现如下:
@Override
public Object getObject(Object key) {
//从缓存中查找对应的缓存项
Object result = null;
@SuppressWarnings("unchecked") // assumed delegate cache is totally managed by this cache
SoftReference<Object> softReference = (SoftReference<Object>) delegate.getObject(key);
if (softReference != null) {//检测缓存中是否有对应的缓存项
result = softReference.get();
if (result == null) {//已经被 GC 回收
delegate.removeObject(key);//从缓存中清除对应的缓存项
} else {
///缓存项的 value 添加到 hardLinksToAvoidGarbageCollection 集合中保存
// See #586 (and #335) modifications need more than a read lock
synchronized (hardLinksToAvoidGarbageCollection) {
hardLinksToAvoidGarbageCollection.addFirst(result);
if (hardLinksToAvoidGarbageCollection.size() > numberOfHardLinks) {
hardLinksToAvoidGarbageCollection.removeLast();
}
}
}
}
return result;
}
1.5 ScheduledCache&LoggingCache&Synchronized&CacheSerializedCache
ScheduledCache 是周期性清理缓存的装饰器,它的 clear Interval 宇段记录了两次缓存清理之 间的时间间隔,默认是一小时, lastClear 字段记录了最近一次清理的时间戳。 ScheduledCache 的 getObject()、 putObject()、 removeObject()等核心方法在执行时,都会根据这两个字段检测是 否需要进行清理操作,清理操作会清空缓存中所有缓存项。
LoggingCache 在 Cache 的基础上提供了日志功能,它通过 hit 宇段和 request 字段记录了 Cache 的命中次数和访问次数。在 LoggingCache.getO均ect()方法中会统计命中次数和访问次数 这两个指标,井按照指定的日志输出方式输出命中率。 LoggingCache 代码比较简单,请读者参 考代码学习。
SynchronizedCache通过在每个方法上添加 synchronized关键字,为Cache添加了同步功能, 有点类似于 JDK 中 Collections 中的 SynchronizedCollection 内部类的实现。 SynchronizedCache 代码比较简单。
Serialized Cache 提供了将 value 对象序列化的功能。 S巳rializedCache 在添加缓存项时,会将 value 对应的 Java 对象进行序列化,井将序列化后的 byte[]数组作为 value 存入缓存 。 Serialized Cache 在获取缓存项时,会将缓存项中的 byte[]数组反序列化成 Java 对象。使用前面 介绍的 Cache 装饰器实现进行装饰之后,每次从缓存中获取同- key 对应的对象时,得到的都 是同一对象,任意一个线程修改该对象都会影 响到其他线程以及缓存中的对象:而 SerializedCache 每次从缓存中获取数据时,都会通过反序列化得到一个全新的对象。
二、CacheKey
在 Cache 中唯一确定一个缓存项需要使用缓存项的 key, MyBatis 中因为涉及动态 SQL 等 多方面因素, 其缓存项的 key 不能仅仅通过一个 String 表示,所以 MyBatis 提供了 CacheKey 类来表示缓存项的 key,在一个 CacheKey 对象中可以封装多个影响缓存项的因素。 CacheKey 中可以添加多个对象,由这些对象共同确定两个 CacheKey 对象是否相同。
CacheKey中的核心字段:
private static final int DEFAULT_MULTIPLIER = 37;
private static final int DEFAULT_HASHCODE = 17;
private final int multiplier;//参与hash计算的乘数 默认是37
private int hashcode;//CacheKey的hash值,在update函数中实时运算出来的
private long checksum;//校验和
private int count;//updateList集合的个数
private List<Object> updateList;//由该集合中的所有对象共同决定两个 CacheKey 是否相同
public CacheKey() {
this.hashcode = DEFAULT_HASHCODE;
this.multiplier = DEFAULT_MULTIPLIER;
this.count = 0;
this.updateList = new ArrayList<>();
}
CacheKey 对象由四个部分构成,也就是说这四部分都 会记录到该 CacheKey 对象的 updateList 集合中:
1、MappedStatement 的 id。
2、指定查询结果集的范围,也就是 RowBounds.offset 和 RowBounds.limit。
3、查询所使用的 SQL 语句,也就是 boundSql.getSql()方法返回的 SQL 语句,其中可能包 含“?”占位符。
4、用户传递给上述 SQL 语句的实际参数值。
在向 CacheKey.updateList 集合中添加对象时,使用的是 CacheKey.update()方法,具体实现 如下:
public CacheKey(Object[] objects) {
this();
updateAll(objects);
}
public void updateAll(Object[] objects) {
for (Object o : objects) {
update(o);
}
}
public void update(Object object) {
//获取object的hash值
int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
//更新count、checksum以及hashcode的值
count++;
checksum += baseHashCode;
baseHashCode *= count;
hashcode = multiplier * hashcode + baseHashCode;
//将对象添加到updateList中
updateList.add(object);
}
CacheKey 重写了 equals()方法和 hashCode()方法,这两个方法使用上面介绍的 count、 checksum、 hashcode、 updateList 比较 CacheKey 对象是否相同,具体实现如下:
@Override
public boolean equals(Object object) {
if (this == object) { //比较是否是同一对象
return true;
}
if (!(object instanceof CacheKey)) {//判断是否是同一类型
return false;
}
final CacheKey cacheKey = (CacheKey) object;
if (hashcode != cacheKey.hashcode) {//判断hashcode是否相同
return false;
}
if (checksum != cacheKey.checksum) {checksum是否相同
return false;
}
if (count != cacheKey.count) {//count是否相同
return false;
}
//顺序比较updateList中元素的hash值是否一致
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;
}
@Override
public int hashCode() {
return hashcode;
}
三 cache装饰者类的使用
CacheBuilder.setStandardDecorators()方法会根据 CacheBuilder 中各个字段的值,为 cache 对 象添加对应的装饰器,具体实现如下:
//缓存模块装饰器的使用
private Cache setStandardDecorators(Cache cache) {
try {
//获取基本的cache对象
MetaObject metaCache = SystemMetaObject.forObject(cache);
//缓存的容量大小
if (size != null && metaCache.hasSetter("size")) {
metaCache.setValue("size", size);
}
//检测是否指定了 clearinterval 字段 (定期清除)
if (clearInterval != null) {
cache = new ScheduledCache(cache);//添加定期清除的装饰器
((ScheduledCache) cache).setClearInterval(clearInterval);
}
//如果是只读缓存
if (readWrite) {
//是否只读,对应添加 SerializedCache 装饰器
cache = new SerializedCache(cache);
}
//默认添加 LoggingCache 和 SynchronizedCache 两个装饰器
cache = new LoggingCache(cache);
cache = new SynchronizedCache(cache);
if (blocking) {//阻塞装饰器
cache = new BlockingCache(cache);
}
return cache;
} catch (Exception e) {
throw new CacheException("Error building standard cache decorators. Cause: " + e, e);
}
}