相信大家对于缓存都不陌生,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… 而且,每扩展一个新的功能,就需要把所有已有的缓存再排列组合一遍,最终的结果就是类爆炸。
组合优于继承。
装饰器模式最大的作用,就是为已有的组件动态地扩展新的功能。
MyBatis缓存模块的设计就采用了装饰器模式。最基础的缓存实现PerpetualCache就是ConcreteComponent,并且在此基础上提供了多种ComponentDecoratorImpl,对缓存的功能进行扩展。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QhRZSaFu-1595151903883)(/Users/zhangshenao/Desktop/mybatis/cache_decorators.png)]
基于这种装饰器模式的设计,想为缓存进行功能的动态扩展就变得十分容易了。上面那个具有多种功能的二级缓存,就可以采用这种方式创建:
Cache cache = new ScheduledCache(new BlockingCache(new LruCache(new PerpetualCache())));
这样一来,客户端就可以任意增加自己想要的缓存功能。相较于继承,装饰器模式使得组件在运行期可以根据需要动态的添加功能,甚至对添加的新功能进行自由的组合,十分灵活且扩展性强。
下面介绍几个缓存装饰器,个人觉得还挺有意思的:
/**
* 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;
}
}
}
/**
* 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;
}
}
/**
* @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;
}
}
既然说到了缓存,就不得不提缓存Key的设计问题。MyBatis涉及到的查询场景十分复杂,查询的操作SQL语句、SQL参数等等信息,都会影响到缓存是否命中,使用简单的String做为缓存Key是肯定不行了,那么该如何设计呢?
MyBatis定义了CacheKey类,封装了所有影响缓存命中的因素,主要包括:
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,则认为是同一个查询操作,可以直接从缓存中获取数据。