MyBatis基础支持层位于 Mybatis 整体架构的最底层,支撑着 Mybatis 的核心处理层,是整个框架的基石。基础支持层中封装了多个较为通用的、独立的模块,不仅仅为 Mybatis 提供基础支撑,也可以在合适的场景中直接复用。
这篇文章介绍MyBatis的缓存模块
MyBatis
作为 一个 强大的持久层 框 架,缓 存是其必不可少的功能之一。MyBatis中的缓 存 是两 层 结 构 的,分为 一级 缓 存、二级 缓 存,但在本质 上是相同的,它 们 使用的都是Cache
接 口的实 现 。
在 MyBatis缓 存模块 中涉及了装 饰 器模式的相关 知识 。
装饰器模式
在实践生产中,新需求在软件的整个生命过程中总是不断出现的。当有新需求出现时,就需要为某些组件添加新的功能来满足这些需求。
添加新功能的方式有很多,我们可以直接修改己有组件的代码并添加相应的新功能,这显然会破坏己有组件的稳定性,修改完成后,整个组件需要重新进行测试,才能上线使用。这种方式显然违反了“开放-封闭”原则。
另一种方式是使用继承方式,我们可以创建子类并在子类中添加新功能实现扩展。*这种方法是静态的,用户不能控制增加行为的方式和时机。而且有些情况下继承是不可行的。
例如己有组件是被
final
关键字修饰的类。另外,如果待添加的新功能存在多种组合,使用继承方式可能会导致大量子类的出现。
例如,有4个待添加的新功能,系统需要动态使用任意多个功能的组合,则需要添加15个子类才能满足全部需求。
装饰器模式能够帮助我们解决上述问题,装饰器可以动态地为对象添加功能,它是基于组合的方式实现该功能的。
在实践中,我们应该尽量使用组合的方式来扩展系统的功能,而非使用继承的方式。设计模式中常见的一句话:组合优于继承。
装饰器模式的类图,以及其中的核心角色:
Component(组件)
组件接口定义了全部组件实现类以及所有装饰器实现的行为。
ConcreteComponent (具体 组 件实 现 类 )
具体组件实现类实现了
Component
接口。通常情况下,具体组件实现类就是被装饰器装饰的原始对象,该类提供了
Component
接口中定义的最基本的功能,其他高级功能或后续添加的新功能,都是通过装饰器的方式添加到该类的对象之上的。Decorator(装饰器)
所有装饰器的父类,它是一个实现了
Component
接口的抽象类,并在其中封装了一个Component
对象,也就是被装饰的对象。而这个被装饰的对象只要是
Component
类型即可,这就实现了装饰器的组合和复用。如下图,装饰器C(ConcreteDecoratorl类型)修饰了装饰器B(ConcreteDecorator2类型)并为其添加功能
W
,而装饰器B(ConcreteDecorator2类型)
又修饰了组件A(ConcreteComponent类型)
并为其添加功能V
。其中,组件对象A提供的是最基本的功能,装饰器B和装饰器C会为组件对象A添加新的功能。
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
接口
使用装饰器模式的优点:
- 相较于继承来说,装饰器模式的灵活性更强,可扩展性也强。正如前面所说,继承方式会导致大量子类的情况。而装饰者模式可以将复杂的功能切分成一个个独立的装饰器,通过多个独立装饰器的动态组合,创建不同功能的组件,从而满足多种不同需求。
- 当有新功能需要添加时,只需要添加新的装饰器实现类,然后通过组合方式添加这个新装饰器即可,无须修改己有类的代码,符合“开放-封闭”原则。
但是,随着添加的新需求越来越多,可能会创建出嵌套多层装饰器的对象,这增加了系统的复杂性,也增加了理解的难度和定位错误的难度。
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接口的基本实现。
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;
假设线程A
在BlockingCache
中未查找到keyA
对应的缓存项时,线程A
会获取keyA
对应的锁,这样后续线程在查找keyA
时会发生阻塞
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对应的数据,而不是再次访问数据库。
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
中缓存项的value
是SoftEntry
对象,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
字段记录了最近一次清理的时间戳。
ScheduledCache
的getObject()
、putObject()
、removeObject()
等核心方法在执行时,都会根据这两个字段检测是否需要进行清理操作,清理操作会清空缓存中所有缓存项。
LoggingCache
在Cache
的基础上提供了日志功能,它通过hit
字段和request
字段记录了Cache
的命中次数和访问次数。
在LoggingCache.getObject()
方法中会统计命中次数和访问次数这两个指标,并按照指定的日志输出方式输出命中率。
SynchronizedCache
通过在每个方法上添加synchronized
关键字,为Cache
添加了同步功能,有点类似于JDK
中Collections
中的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技术内幕》

相关推荐
版权声明
- 本文链接: https://www.cayzlh.com/2019/10/31/fce458a9.html
- 版权声明: 文章内容仅用作学习和分享,如有雷同,纯属拷贝。
留言区