MyBatis基础支持层位于 Mybatis 整体架构的最底层,支撑着 Mybatis 的核心处理层,是整个框架的基石。基础支持层中封装了多个较为通用的、独立的模块,不仅仅为 Mybatis 提供基础支撑,也可以在合适的场景中直接复用。

整体架构

这篇文章介绍MyBatis的缓存模块

MyBatis作为 一个 强大的持久层 框 架,缓 存是其必不可少的功能之一。MyBatis中的缓 存 是两 层 结 构 的,分为 一级 缓 存二级 缓 存,但在本质 上是相同的,它 们 使用的都是Cache接 口的实 现 。

在 MyBatis缓 存模块 中涉及了装 饰 器模式的相关 知识 。

装饰器模式

在实践生产中,新需求在软件的整个生命过程中总是不断出现的。当有新需求出现时,就需要为某些组件添加新的功能来满足这些需求。

添加新功能的方式有很多,我们可以直接修改己有组件的代码并添加相应的新功能,这显然会破坏己有组件的稳定性,修改完成后,整个组件需要重新进行测试,才能上线使用。这种方式显然违反了“开放-封闭”原则。

另一种方式是使用继承方式,我们可以创建子类并在子类中添加新功能实现扩展。*这种方法是静态的,用户不能控制增加行为的方式和时机。而且有些情况下继承是不可行的。

  • 例如己有组件是被final关键字修饰的类。

  • 另外,如果待添加的新功能存在多种组合,使用继承方式可能会导致大量子类的出现。

    例如,有4个待添加的新功能,系统需要动态使用任意多个功能的组合,则需要添加15个子类才能满足全部需求。

装饰器模式能够帮助我们解决上述问题,装饰器可以动态地为对象添加功能,它是基于组合的方式实现该功能的。

在实践中,我们应该尽量使用组合的方式来扩展系统的功能,而非使用继承的方式。设计模式中常见的一句话:组合优于继承。

装饰器模式的类图,以及其中的核心角色:

image-20200611103644916

  • Component(组件)

    组件接口定义了全部组件实现类以及所有装饰器实现的行为。

  • ConcreteComponent (具体 组 件实 现 类 )

    具体组件实现类实现了Component接口。

    通常情况下,具体组件实现类就是被装饰器装饰的原始对象,该类提供了Component接口中定义的最基本的功能,其他高级功能或后续添加的新功能,都是通过装饰器的方式添加到该类的对象之上的。

  • Decorator(装饰器)

    所有装饰器的父类,它是一个实现了Component接口的抽象类,并在其中封装了一个Component对象,也就是被装饰的对象。

    而这个被装饰的对象只要是Component类型即可,这就实现了装饰器的组合和复用。

    如下图,装饰器**C(ConcreteDecoratorl类型)修饰了装饰器B(ConcreteDecorator2类型)**并为其添加功能W,而装饰器B(ConcreteDecorator2类型)又修饰了组件A(ConcreteComponent类型)并为其添加功能V

    其中,组件对象A提供的是最基本的功能,装饰器B和装饰器C会为组件对象A添加新的功能。

    image-20200611105658387

  • ConcreteDecorator

    具体的装饰器实现类,该实现类要向被装饰对象添加某些功能。如上图,装饰器B、C就是该角色,被装饰的对象只要是Component类型即可。
    JavaIO包中,大量应用了装饰器模式,我们在使用JavaIO包读取文件时,经常会看到如下代码:

    BufferedlnputStream bis = 
    new BufferedlnputStream( new FilelnputStream(new File("D:/test.txt")));

FilelnputStream并没有缓冲功能,每次调用其read()方法时都会向操作系统发起相应的系统调用,当读取大量数据时,就会导致操作系统在用户态和内核态之间频繁切换,性能较低。

BufferedlnputStream是提供了缓冲功能的装饰器,每次调用其read()方法时,会预先从文件中获取一部分数据并缓存到BufferedlnputStream的缓冲区中,后面连续的几次读取可以直接从缓冲区中获取数据,直到缓冲区数据耗尽才会重新从文件中读取数据,这样就可以减少用户态和内核态的切换,提高了读取的性能。

MyBatis的缓存模块中,使用了装饰器模式的变体,其中将Decorator接口和Component接口合并为一个Component接口

image-20200611114247838

使用装饰器模式的优点:

  • 相较于继承来说,装饰器模式的灵活性更强,可扩展性也强。正如前面所说,继承方式会导致大量子类的情况。而装饰者模式可以将复杂的功能切分成一个个独立的装饰器,通过多个独立装饰器的动态组合,创建不同功能的组件,从而满足多种不同需求。
  • 当有新功能需要添加时,只需要添加新的装饰器实现类,然后通过组合方式添加这个新装饰器即可,无须修改己有类的代码,符合“开放-封闭”原则。

但是,随着添加的新需求越来越多,可能会创建出嵌套多层装饰器的对象,这增加了系统的复杂性,也增加了理解的难度和定位错误的难度。

Cache接口及其实现

MyBatis的缓存模块在org.apache.ibatis.cache包下,其中Cache接口是缓存模块的中最核心的接口,它定义了所有缓存的基本行为。

Cache

public interface Cache {

/**
* 返回该缓存对应的id
* @return The identifier of this cache
*/
String getId();

/**
* 向缓存中添加数据,一般情况下Key是CacheKey,value是查询结果
* @param key Can be any object but usually it is a {@link CacheKey}
* @param value The result of a select.
*/
void putObject(Object key, Object value);

/**
* 从缓存中获取数据
* @param key The key
* @return The object stored in the cache.
*/
Object getObject(Object key);

/**
* 删除Key对应的缓存
* As of 3.3.0 this method is only called during a rollback
* for any previous value that was missing in the cache.
* This lets any blocking cache to release the lock that
* may have previously put on the key.
* A blocking cache puts a lock when a value is null
* and releases it when the value is back again.
* This way other threads will wait for the value to be
* available instead of hitting the database.
*
*
* @param key The key
* @return Not used
*/
Object removeObject(Object key);

/**
* 清空缓存
* Clears this cache instance
*/
void clear();

/**
* 缓存项的个数,不会被MyBatis核心代码调用
* Optional. This method is not called by the core.
*
* @return The number of elements stored in the cache (not its capacity).
*/
int getSize();

/**
* 获取读写锁,不会被MyBatis核心代码调用
* Optional. As of 3.2.6 this method is no longer called by the core.
*
* Any locking needed by the cache must be provided internally by the cache provider.
*
* @return A ReadWriteLock
*/
ReadWriteLock getReadWriteLock();

}

Cache接口的实现类有多个,大部分都是装饰器,只有PerpetualCache提供了Cache接口的基本实现。

image-20200611120028350

PerpetualCache

**PerpetualCache在缓存模块中扮演着ConcreteComponent的角色**,其实现比较简单,底层使用HashMap记录缓存项,也是通过该HashMap对象的方法实现的Cache接口中定义的相应方法。

public class PerpetualCache implements Cache {

private final String id;

private Map<Object, Object> cache = new HashMap<Object, Object>();

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();
}

@Override
public ReadWriteLock getReadWriteLock() {
return null;
}

@Override
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 otherCache = (Cache) o;
return getId().equals(otherCache.getId());
}

@Override
public int hashCode() {
if (getId() == null) {
throw new CacheException("Cache instances require an ID.");
}
return getId().hashCode();
}

}

下面来介绍org.apache.ibatis.cache.decorators包下提供的装饰器,它们都直接实现了Cache接口,扮演着ConcreteDecorator的角色。

这些装饰器会在PerpetualCache的基础上提供一些额外的功能,通过多个组合后满足一个特定的需求,后面介绍二级缓存时,会见到这些装饰器是如何完成动态组合的。

BlockingCache

BlockingCache是阻塞版本的缓存装饰器,它会保证只有一个线程到数据库中查找指定key对应的数据。

// 阻塞超时时间
private long timeout;
// 被装饰的底层Cache对象
private final Cache delegate;
// 每个Key都有对应的ReentrantLock对象
private final ConcurrentHashMap<Object, ReentrantLock> locks;

假设线程ABlockingCache中未查找到keyA对应的缓存项时,线程A会获取keyA对应的锁,这样后续线程在查找keyA时会发生阻塞

image-20200616092820328

BlockingCache.getObject(Object key)

public Object getObject(Object key) {
// 获取key对应的锁
acquireLock(key);
Object value = delegate.getObject(key);
if (value != null) {
// 如果缓存有key对应的缓存项,则释放锁
releaseLock(key);
}
return value;
}

acquireLock(Object key)

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("...");
}
} catch (InterruptedException e) {
throw new CacheException("...");
}
} else {
lock.lock();
}
}

再来看一下getLockForKey方法

private ReentrantLock getLockForKey(Object key) {
ReentrantLock lock = new ReentrantLock(); // 创建ReentrantLock对象
// 试添加到locks集合中,如果locks集合中已经有了相应的ReentrantLock对象,则使用locks集合
// 中的ReentrantLock对象
ReentrantLock previous = locks.putIfAbsent(key, lock);
return previous == null ? lock : previous;
}

假设线程A从数据库中查找到keyA对应的结果对象后,将结果对象放入到BlockingCache中,此时线程A会释放keyA对应的锁,唤醒阻塞在该锁上的线程。

其他线程即可从BlockingCache中获取keyA对应的数据,而不是再次访问数据库。

image-20200618093048912

BlockingCache.putObject()

@Override
public void putObject(Object key, Object value) {
try {
// 向缓存中添加缓存项
delegate.putObject(key, value);
} finally {
releaseLock(key);// 释放锁
}
}

BlockingCache.releaseLock(Object key)

private void releaseLock(Object key) {
ReentrantLock lock = locks.get(key);
if (lock.isHeldByCurrentThread()) { // 判断锁是否被当前线程持有
// 释放锁
lock.unlock();
}
}

FifoCache&LruCache

在很多场景中,为了控制缓存的大小,系统需要按照一定的规则清理缓存。

FifoCache是先入先出版本的装饰器,**当向缓存添加数据时,如果缓存项的个数己经达到上限,则会将缓存中最老(即最早进入缓存)的缓存项删除。**

FifoCache中字段的含义:

// 被装饰的底层Cache对象
private final Cache delegate;
// 用于记录key进入缓存的先后顺序,使用的是LinkedList<Object>类型的几个对象
private final Deque<Object> keyList;
// 记录缓存项的上限,如果超过,则清理最老的缓存
private int size;

FifoCache.getObject()removeObject()方法的实现都是直接调用底层Cache对象的对应方法。

FifoCache.putObject()方法中会完成缓存项个数的检测以及缓存的清理操作。

public void putObject(Object key, Object value) {
// 检测并清理缓存
cycleKeyList(key);
// 添加缓存项
delegate.putObject(key, value);
}

private void cycleKeyList(Object key) {
// 记录key
keyList.addLast(key);
if (keyList.size() > size) { // 如果达到缓存上限,则清理最老的缓存项
Object oldestKey = keyList.removeFirst();
delegate.removeObject(oldestKey);
}
}

LruCache是按照近期最少使用算法**(LeastRecentlyUsed,LRU)**进行缓存清理的装饰器,在需要清理缓存时,它会清除最近最少使用的缓存项。

// 被装饰的底层Cache对象
private final Cache delegate;
// LinkedHashMap<Object,Object>类型对象,它是一个有序的HashMap,用于记录key最近的使用情况
private Map<Object, Object> keyMap;
// 记录最少被使用的缓存项的key
private Object eldestKey;

LruCache的构造函数中默认设置的缓存大小是1024,我们可以通过其setSize()方法重新设置缓存大小

public void setSize(final int size) {
// 重新设置缓存大小的时候, 会充值keyMap字段
// LinkedHashMap构造函数的第三个参数,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;
}
};
}

LruCache.getObject()方法除了返回缓存项,还会调用keyMap.get()方法修改key的顺序,表示指定的key最近被使用。

LruCache.putObject()方法除了添加缓存项,还会将eldestKey字段指定的缓存项清除掉。

@Override
public void putObject(Object key, Object value) {
// 修改LinkedHashMap中记录的顺序
delegate.putObject(key, value);
cycleKeyList(key);
}

@Override
public Object getObject(Object key) {
keyMap.get(key); //touch
// 删除最久未使用的缓存项
return delegate.getObject(key);
}

private void cycleKeyList(Object key) {
keyMap.put(key, key);
if (eldestKey != null) { // eldestKey不为空,即缓存已达到上限
// 删除最久未使用的缓存
delegate.removeObject(eldestKey);
eldestKey = null;
}
}

SoftCache&WeakCache

先了解一下Java提供的4种引用类型

SoftCache中各个字段的含义:

// ReferenceQueue,引用队列,用于记录已经被GC回收的缓存项所对应的SoftEntry对象
private final Deque<Object> hardLinksToAvoidGarbageCollection;
// 在SoftCache中,最近使用的一部分缓存项不会被GC回收,这就是通过将其value添加到
// hardLinksToAvoidGarbageCollection集合中实现的(即有强引用指向其value)
// hardLinksToAvoidGarbageCollection集合是LinkedList<Object>类型
private final ReferenceQueue<Object> queueOfGarbageCollectedEntries;
// 底层被装饰的底层Cache对象
private final Cache delegate;
// 强连接的个数,默认值是256
private int numberOfHardLinks;

SoftCache中缓存项的valueSoftEntry对象,SoftEntry继承了SoftReference,其中指向key的引用是强引用,而指向value的引用是软引用。

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; // 强引用
}
}

SoftCache.putObject()方法除了向缓存中添加缓存项,还会清除己经被GC回收的缓存项,其具体实现如下:

public void putObject(Object key, Object value) {
// 清除已被GC回收的缓存项
removeGarbageCollectedItems();
// 向缓存中添加缓存项
delegate.putObject(key, new SoftEntry(key, value, queueOfGarbageCollectedEntries));
}

private void removeGarbageCollectedItems() {
SoftEntry sv;
// 遍历queueOfGarbageCollectedEntries集合
while ((sv = (SoftEntry) queueOfGarbageCollectedEntries.poll()) != null) {
// 将已经被GC回收的value对象对应的缓存项清除
delegate.removeObject(sv.key);
}
}

SoftCache.getObject()方法除了从缓存中查找对应的value,处理被GC回收的value对应的缓存项,还会更新hardLinksToAvoidGarbageCollection集合

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(); // 获取SoftReference引用的value
if (result == null) {// 已经被GC回收
delegate.removeObject(key); // 从缓存中清除对应的缓存项
} else { // 未被GC回收
// See #586 (and #335) modifications need more than a read lock
synchronized (hardLinksToAvoidGarbageCollection) {
// 缓存项的value添加到hardLinksToAvoidGarbageCollection集合中保存
hardLinksToAvoidGarbageCollection.addFirst(result);
if (hardLinksToAvoidGarbageCollection.size() > numberOfHardLinks) {
// 超过numberOfHardLinks,则将最老的缓存项从hardLinksToAvoidGarbageCollection集合中清除,有点类似于先进先出队列
hardLinksToAvoidGarbageCollection.removeLast();
}
}
}
}
return result;
}

SoftCache.removeObject()方法在清除缓存项之前,也会调用removeGarbageCollectedItems()方法清理被GC回收的缓存项。

SoftCache.clear()方法首先清理hardLinksToAvoidGarbageCollection集合,然后清理被GC回收的缓存项,最后清理底层delegate缓存中的缓存项。

public void clear() {
synchronized (hardLinksToAvoidGarbageCollection) {
hardLinksToAvoidGarbageCollection.clear(); // 清理强引用集合
}
removeGarbageCollectedItems(); // 清理被GC回收的缓存项
delegate.clear(); // 清理底层delegate缓存中的缓存项
}

WeakCache的实现与SoftCache基本类似,唯一的区别在于其中使用WeakEntry(继承自WeakReference)封装真正的value对象,其他实现完全一样。

others

ScheduledCache是周期性清理缓存的装饰器,它的clearlnterval字段记录了两次缓存清理之间的时间间隔,默认是一小时,lastClear字段记录了最近一次清理的时间戳。

ScheduledCachegetObject()putObject()removeObject()等核心方法在执行时,都会根据这两个字段检测是否需要进行清理操作,清理操作会清空缓存中所有缓存项。

LoggingCacheCache的基础上提供了日志功能,它通过hit字段和request字段记录了Cache的命中次数和访问次数。

LoggingCache.getObject()方法中会统计命中次数和访问次数这两个指标,并按照指定的日志输出方式输出命中率。

SynchronizedCache通过在每个方法上添加synchronized关键字,为Cache添加了同步功能,有点类似于JDKCollections中的SynchronizedCollection内部类的实现。

SerializedCache提供了将value对象序列化的功能。

SerializedCache在添加缓存项时,会将value对应的Java对象进行序列化,并将序列化后的byte[]数组作为value存入缓存。

SerializedCache在获取缓存项时,会将缓存项中的byte[]数组反序列化成Java对象。

使用前面介绍的Cache装饰器实现进行装饰之后,每次从缓存中获取同一key对应的对象时,得到的都是同一对象,任意一个线程修改该对象都会影响到其他线程以及缓存中的对象;而SerializedCache每次从缓存中获取数据时,都会通过反序列化得到一个全新的对象。**SerializedCache使用的序列化方式是Java原生序列化。**

CacheKey

Cache中唯一确定一个缓存项需要使用缓存项的key,MyBatis中因为涉及动态SQL等多方面因素,其缓存项的key不能仅仅通过一个String表示,所以MyBatis提供了CacheKey类来表示缓存项的key,在一个CacheKey对象中可以封装多个影响缓存项的因素。

CacheKey中可以添加多个对象,由这些对象共同确定两个CacheKey对象是否相同。

// 参与计算hashcode,默认值37
private final int multiplier;
// CacheKey的hashcode,初始值是37
private int hashcode;
// 校验和
private long checksum;
// updateList集合的个数
private int count;
// 由该集合中的所有对象共同决定两个CacheKey是否相同
private List<Object> updateList;

在向CacheKey.updateList集合中添加对 象时 ,使用的是CacheKey.update()方法:

public void update(Object object) {
int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
// 重新计算count、checksum和hashcode的值
count++;
checksum += baseHashCode;
baseHashCode *= count;

hashcode = multiplier * hashcode + baseHashCode;
// 将object添加到updateList集合中
updateList.add(object);
}

参考

  • 《MyBatis技术内幕》

  • 部分图片来源——《MyBatis技术内幕》