MyBatis设计思想(4)——缓存模块
一. 缓存概述
相信大家对于缓存都不陌生,MyBatis也提供了缓存的功能,在执行查询语句时首先尝试从缓存获取,避免频繁与数据库交互,大大提升了查询效率。MyBatis有所谓的一级缓存和二级缓存,这个会在后面的核心流程中详细阐述,这里仅讨论缓存的内部实现。
首先看下MyBatis的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();
}
我们知道,缓存的本质其实就是一个Map,MyBatis的缓存最基础的实现PerpetualCache,也是使用了一个HashMap来保存数据:
/**
* @author Clinton Begin
*
* 缓存的基础实现,使用HashMap来保存数据
*/
public class PerpetualCache implements Cache {
private final String id; //缓存的唯一id
private final Map<Object, Object> cache = new HashMap<>(); //内部使用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();
}
@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();
}
这个缓存的实现看上去平平无奇,几乎任何人都能写得出来。那么现在问题来了,作为一个成熟的ORM框架,MyBatis势必要为缓存提供各种额外的扩展功能,比如淘汰策略、锁同步功能、定时清空功能、防击穿、打印日志等。那么,如何在缓存的基础实现上,动态扩展这些功能呢?
二. 通过继承扩展
想要对一个类进行功能上的扩展,我们第一时间就会想到继承。的确,通过继承可以很方便地在现有的类上增加额外的功能。如果我们想要为缓存增加LRU淘汰策略,只需要新建一个LruCache类,继承PerpetualCache即可。同理,想要具有打印日志功能的缓存,就需要再创建一个LoggingCache类,这种解决方案看上去可以满足需求。
但是问题是,缓存的能力是动态组合和扩展的。在实际使用中,常常会见到下面这种形式的缓存配置:
<!--开启二级缓存配置-->
<cache eviction="LRU" flushInterval="60000" blocking="true" size="512"/>
这里开启了二级缓存,设置了容量上限的512,淘汰策略是LRU,每60s清空,且缓存为空时通过阻塞式从DB中查询数据,避免大量缓存击穿。这样就要求缓存实现类动态扩展LRU、定时清空、阻塞查询的功能。这样一来,如果依然通过继承的方式实现,就需要再创建LruScheduledBlockingCache类。而且,由于所有功能是动态增加的,你事先并不知道客户端会选择哪几个功能,那么就需要提前把所有功能排列组合地实现一遍,如LruScheduledCache、ScheduledBlockingCache、LruBlockingCache… 而且,每扩展一个新的功能,就需要把所有已有的缓存再排列组合一遍,最终的结果就是类爆炸。
组合优于继承。
三. 装饰器模式
装饰器模式最大的作用,就是为已有的组件动态地扩展新的功能。
- IComponent:定义了所有组件和装饰器的公共行为。
- ConcreteComponent:具体的组件,实现IComponent,是需要被装饰的原始对象,新功能或者附加功能都是通过装饰器添加到该类的对象上的。
- ComponentDecorator(Optional):抽象装饰器,定义了装饰器的核心行为。
- ComponentDecoratorImpl:具体的装饰器,对ComponentDecorator进行功能的扩展。
MyBatis缓存模块的设计就采用了装饰器模式。最基础的缓存实现PerpetualCache就是ConcreteComponent,并且在此基础上提供了多种ComponentDecoratorImpl,对缓存的功能进行扩展。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QhRZSaFu-1595151903883)(/Users/zhangshenao/Desktop/mybatis/cache_decorators.png)]
基于这种装饰器模式的设计,想为缓存进行功能的动态扩展就变得十分容易了。上面那个具有多种功能的二级缓存,就可以采用这种方式创建:
Cache cache = new ScheduledCache(new BlockingCache(new LruCache(new PerpetualCache())));
这样一来,客户端就可以任意增加自己想要的缓存功能。相较于继承,装饰器模式使得组件在运行期可以根据需要动态的添加功能,甚至对添加的新功能进行自由的组合,十分灵活且扩展性强。
四. 几个有趣的缓存装饰器
下面介绍几个缓存装饰器,个人觉得还挺有意思的:
-
LruCache
MyBatis很好地使用了JDK的LinkedHashMap,LinkedHashMap支持按照访问顺序排序,将最近被访问到的元素放在队列头部,这样就天然支持了LRU。
/** * Lru (least recently used) cache decorator. * * @author Clinton Begin * * LRU缓存装饰器,依赖了LinkedHashMap的实现 */ 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); } @Override public String getId() { return delegate.getId(); } @Override public int getSize() { return delegate.getSize(); } public void setSize(final int size) { //这里使用了LinkedHashMap保存所有Cache Key //LinkedHashMap支持按照访问顺序排序,会将最近被访问到的元素放在队列头部 keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) { private static final long serialVersionUID = 4267176411845948333L; @Override protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) { boolean tooBig = size() > size; if (tooBig) { eldestKey = eldest.getKey(); //如果缓存大小超过容量上限,则记录待淘汰的key,在下次插入元素时删除 } return tooBig; } }; } @Override public void putObject(Object key, Object value) { delegate.putObject(key, value); //插入元素后,删除过期的元素 cycleKeyList(key); } @Override public Object getObject(Object key) { keyMap.get(key); // touch return delegate.getObject(key); } @Override public Object removeObject(Object key) { return delegate.removeObject(key); } @Override public void clear() { delegate.clear(); keyMap.clear(); } //删除过期key private void cycleKeyList(Object key) { keyMap.put(key, key); if (eldestKey != null) { delegate.removeObject(eldestKey); eldestKey = null; } } } -
BlockingCache
BlockingCache的作用是,当缓存Miss时,对线程加锁,保证同一时刻只有一个线程去DB执行查询操作,这样就避免了高并发场景下,缓存失效造成的大量击穿。
/** * Simple blocking decorator * * Simple and inefficient version of EhCache's BlockingCache decorator. * 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. * * @author Eduardo Macarron * * 缓存的阻塞装饰器: 当缓存Miss时,对线程加锁,保证同时只有一个线程去DB执行查询操作,这样就避免了高并发场景下,缓存失效造成的大量击穿。 * */ public class BlockingCache implements Cache { private long timeout; private final Cache delegate; private final ConcurrentHashMap<Object, ReentrantLock> locks; //使用ConcurrentHashMap,按照Cache Key粒度加锁 public BlockingCache(Cache delegate) { this.delegate = delegate; this.locks = new ConcurrentHashMap<>(); } @Override public String getId() { return delegate.getId(); } @Override public int getSize() { return delegate.getSize(); } @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) { // despite of its name, this method is called only to release locks releaseLock(key); return null; } @Override public void clear() { delegate.clear(); } private ReentrantLock getLockForKey(Object key) { return locks.computeIfAbsent(key, k -> new ReentrantLock()); } 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("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 void releaseLock(Object key) { ReentrantLock lock = locks.get(key); if (lock.isHeldByCurrentThread()) { lock.unlock(); } } public long getTimeout() { return timeout; } public void setTimeout(long timeout) { this.timeout = timeout; } }但是,BlockingCache是每次查询时都会进行加锁操作,尽管是根据Cache Key的细粒度锁,但是对性能还是有一定的影响,这个类的作者Eduardo Macarron也说了,这是一个简单且低效的版本。
-
ScheduledCache
ScheduledCache的作用就是增加了缓存的定时清空功能。这个清空是lazy的,即在每次get和put操作时,会校验距离上次执行clear操作的时间是否已超过clearInterval。如果超过,则执行一次clear。
/** * @author Clinton Begin * * 具有定时清空功能的缓存装饰器 * lazy模式,在每次get和put操作时,会校验距离上次执行clear操作的时间是否已超过clearInterval。如果超过,则执行一次clear */ public class ScheduledCache implements Cache { private final Cache delegate; protected long clearInterval; protected long lastClear; public ScheduledCache(Cache delegate) { this.delegate = delegate; this.clearInterval = TimeUnit.HOURS.toMillis(1); this.lastClear = System.currentTimeMillis(); } public void setClearInterval(long clearInterval) { this.clearInterval = clearInterval; } @Override public String getId() { return delegate.getId(); } @Override public int getSize() { clearWhenStale(); return delegate.getSize(); } @Override public void putObject(Object key, Object object) { clearWhenStale(); delegate.putObject(key, object); } @Override public Object getObject(Object key) { return clearWhenStale() ? null : delegate.getObject(key); } @Override public Object removeObject(Object key) { clearWhenStale(); return delegate.removeObject(key); } @Override public void clear() { lastClear = System.currentTimeMillis(); delegate.clear(); } @Override public int hashCode() { return delegate.hashCode(); } @Override public boolean equals(Object obj) { return delegate.equals(obj); } //校验清空时间 private boolean clearWhenStale() { if (System.currentTimeMillis() - lastClear > clearInterval) { clear(); return true; } return false; } }
五. CacheKey的设计
既然说到了缓存,就不得不提缓存Key的设计问题。MyBatis涉及到的查询场景十分复杂,查询的操作SQL语句、SQL参数等等信息,都会影响到缓存是否命中,使用简单的String做为缓存Key是肯定不行了,那么该如何设计呢?
MyBatis定义了CacheKey类,封装了所有影响缓存命中的因素,主要包括:
- mappedStatment的id(Cache id)
- 指定查询结果集的范围(分页信息)
- 查询所使用的SQL语句
- 用户传递给SQL语句的实际参数值
CacheKey封装了这些信息,并重写了hashCode()和equals()方法:
/**
* @author Clinton Begin
*
* MyBatis定义的CacheKey
*/
public class CacheKey implements Cloneable, Serializable {
private static final long serialVersionUID = 1146682552656046210L;
public static final CacheKey NULL_CACHE_KEY = new CacheKey() {
@Override
public void update(Object object) {
throw new CacheException("Not allowed to update a null cache key instance.");
}
@Override
public void updateAll(Object[] objects) {
throw new CacheException("Not allowed to update a null cache key instance.");
}
};
private static final int DEFAULT_MULTIPLIER = 37;
private static final int DEFAULT_HASHCODE = 17;
private final int multiplier; //乘积因子
private int hashcode; //hashCode
private long checksum; //校验和
private int count; //影响因素数量
// 8/21/2017 - Sonarlint flags this as needing to be marked transient. While true if content is not serializable, this
// is not always true and thus should not be marked transient.
private List<Object> updateList; //影响因素
public CacheKey() {
this.hashcode = DEFAULT_HASHCODE;
this.multiplier = DEFAULT_MULTIPLIER;
this.count = 0;
this.updateList = new ArrayList<>();
}
public CacheKey(Object[] objects) {
this();
updateAll(objects);
}
public int getUpdateCount() {
return updateList.size();
}
//每次增加CacheKey的影响因素,都会重新计算一遍内部的各种校验值
public void update(Object object) {
int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
count++;
checksum += baseHashCode;
baseHashCode *= count;
hashcode = multiplier * hashcode + baseHashCode;
updateList.add(object);
}
public void updateAll(Object[] objects) {
for (Object o : objects) {
update(o);
}
}
//判断两个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) {
return false;
}
if (checksum != cacheKey.checksum) {
return false;
}
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;
}
@Override
public int hashCode() {
return hashcode;
}
@Override
public String toString() {
StringJoiner returnValue = new StringJoiner(":");
returnValue.add(String.valueOf(hashcode));
returnValue.add(String.valueOf(checksum));
updateList.stream().map(ArrayUtil::toString).forEach(returnValue::add);
return returnValue.toString();
}
@Override
public CacheKey clone() throws CloneNotSupportedException {
CacheKey clonedCacheKey = (CacheKey) super.clone();
clonedCacheKey.updateList = new ArrayList<>(updateList);
return clonedCacheKey;
}
}
如果两个CacheKey的hashCode()相等,且equals()方法返回true,则认为是同一个查询操作,可以直接从缓存中获取数据。