Mybatis查询,默认开启了一级缓存,二级缓存需要手动开启。针对于缓存,Mybatis提供了Cache这个组件进行对查询出来的数据进行缓存,而Cache作为Mybatis混存的基本组件,所以想要对其功能进行拓展但是又不能违反开闭原则,所以将采用装饰器模式。
浅谈装饰器模式
- Component(组件)*:组件接口定义了全部组件实现类以及所有装饰器实现的行为
- ConcreteDecorator(具体组件实现类)*:具体组件实现类实现了Component接口,通常情况下具体组件实现类就是被装饰器装饰的原始对象,该类提供了Component接口定义的最基本功能,其余需要拓展的其他功能都需要通过装饰器对组件进行装饰
- Decorate(装饰器)*:所有装饰器的父类,实现了一个Compoent接口的抽象类,并且在其中还封装了一个Component对象,也就是被装饰的对象
Cache
相当于是装饰器里最基本组件,被装饰器装饰的对象,拓展其实现的功能
public interface Cache {
// 获取缓存对象ID
String getId();
// 添加缓存
void putObject(Object key, Object value);
// 根据缓存对象ID去获取对象
Object getObject(Object key);
// 删除缓存
Object removeObject(Object key);
// 清空缓存
void clear();
// 获取缓存的个数
int getSize();
// 获取读写锁
ReadWriteLock getReadWriteLock();
}
实现类就是通过不断地装饰将Cache组件层级注入,多重装饰
PerpetualCache 永久缓存
相当于是实现了需要被装饰的组件的最基本的实现类,
public class PerpetualCache implements Cache {
// Cache对象的唯一标识
private final String id;
// 声明了一个HashMap去当作缓存
private Map<Object, Object> cache = new HashMap<>();
...
@Override
...
BlockingCache 阻塞缓存
阻塞式的缓存装饰器,保证只有一个线程到数据库中查找指定key对应的数据,
public class BlockingCache implements Cache {
// 设置缓存的超时时间
private long timeout;
// 由于使用了装饰器模式,所以需要将Cache组件注入到装饰器中
private final Cache delegate;
// 使用ConcurrentHashMap,支持检索的完全并发性和更新的高预期并发性的哈希表,每个Key都有对应的ReentrantLock对象
private final ConcurrentHashMap<Object, ReentrantLock> locks;
}
由下图可知,当多个线程去查询BlockingCache会被阻塞,因为BlockingCache是单线程操作,需要等待,查询成功后才会释放锁唤醒阻塞在该锁上的线程。
而对于BlockingCache设置缓存,由于BlockingCache通过ConcurrentHashMap去加载缓存数据,所以生成锁和key的规则由ConcurrentHashMap决定
- BlockingCache生成缓存
@Override
public void putObject(Object key, Object value) {
try {
delegate.putObject(key, value);
} finally {
// 如果添加缓存失败,则会释放锁
releaseLock(key);
}
}
- BlockingCache读取缓存
@Override
public Object getObject(Object key) {
// 获得锁
acquireLock(key);
// 查询Key
Object value = delegate.getObject(key);
// 缓存有key对应的缓存项,释放锁否则继续持有锁
if (value != null) {
releaseLock(key);
}
return value;
}
- Cache对象读取缓存的时候会涉及到ConcurrentHashMap的获取Key,
private ReentrantLock getLockForKey(Object key) {
// 如果指定的键尚未与锁相关联,则尝试使用给定的映射函数计算其值,并将其输入到此映射中,除非null,将会报错 。
return locks.computeIfAbsent(key, k -> new ReentrantLock());
}
// 获得锁
private void acquireLock(Object key) {
// 根据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();
}
}
由下图,假设线程A 先发起请求对数据进行查询,线程A获取BlockingCache的锁,先访问缓存,由于缓存中没有数据所以将查询数据库,将数据库里的数据查询出来并加载到缓存中,并且释放线程A的锁,同时唤醒阻塞在Blocking Cache上的其中一条线程,如果其他线程查询的内容和线程A查询的数据一致则会通过KeyA在Blocking Cache获取数据,而不是再次访问数据库。
ScheduledCache 周期性清理缓存装饰器
设置了定期清理缓存的时间,lastClear记录了最后一次清理缓存的时间,在默认设置的时间间隔之后会再次清理缓存
public class ScheduledCache implements Cache {
// 使用装饰器模式,对Cache组件进行装饰
private final Cache delegate;
// 明确的时间间隔
protected long clearInterval;
// 最后的清理时间
protected long lastClear;
public ScheduledCache(Cache delegate) {
this.delegate = delegate;
// 1 hour默认清理缓存的时间间隔是一小时
this.clearInterval = 60 * 60 * 1000;
this.lastClear = System.currentTimeMillis();
}
}
FifoCache:FIFO(先入先出)缓存装饰器。
public class FifoCache implements Cache {
private final Cache delegate;
// Deque(双端队列),支持两端元素插入和移除的线性集合。 当使用deque作为队列时,FIFO(先进先出)行为的结果
private final Deque<Object> keyList;
// 缓存的数量的上限
private int size;
public FifoCache(Cache delegate) {
this.delegate = delegate;
this.keyList = new LinkedList<>();
this.size = 1024;
}
}
// 加载缓存数据,
@Override
public void putObject(Object key, Object value) {
// 循环键列表,判断List是否占满了,占满了则删除第一个数据
cycleKeyList(key);
delegate.putObject(key, value);
}
LruCache : 最近最少使用的缓存装饰器
public class LruCache implements Cache {
private final Cache delegate;
// 一个有序的HashMap保存每个缓存对象的加载顺序
private Map<Object, Object> keyMap;
// 最早加载进来的缓存对象
private Object eldestKey;
public LruCache(Cache delegate) {
this.delegate = delegate;
// 设置缓存的大小为1024
setSize(1024);
}
- LruCache设置缓存,使用LinkedHashMap
public void setSize(final int size) {
// 新建一个LinkedHashMap,三个参数分别为:初始容量初始容量,装载因子装载因子,排序顺序(为TRUE时是顺序
keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
private static final long serialVersionUID = 4267176411845948333L;
//判断加载的缓存数量是否超过缓存List的大小
@Override
protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
boolean tooBig = size() > size;
if (tooBig) {
eldestKey = eldest.getKey();
}
return tooBig;
}
};
}
SoftCache : 软引用缓存装饰器
public class SoftCache implements Cache {
private final Cache delegate;
// 硬连接,避免垃圾回收,将需要缓存的SoftEntry加载到该集合中
private final Deque<Object> hardLinksToAvoidGarbageCollection;
// 垃圾收集项队列用来记录已经被GC回收的缓存SoftEntry对象
private final ReferenceQueue<Object> queueOfGarbageCollectedEntries;
// 强连接的个数,默认是256个
private int numberOfHardLinks;
public SoftCache(Cache delegate) {
this.delegate = delegate;
this.numberOfHardLinks = 256;
this.hardLinksToAvoidGarbageCollection = new LinkedList<>();
this.queueOfGarbageCollectedEntries = new ReferenceQueue<>();
}
}
LoggingCache :日志缓存装饰器
会记录缓存被访问的次数和缓存被命中的次数,并且按照自定的日志规则返回这两个值
public class LoggingCache implements Cache {
// 添加日志组件
private final Log log;
// 被装饰对象的组件
private final Cache delegate;
// 请求的次数
protected int requests = 0;
// 缓存击中的次数
protected int hits = 0;
public LoggingCache(Cache delegate) {
this.delegate = delegate;
this.log = LogFactory.getLog(getId());
}
}
SynchronizedCache : 同步缓存装饰器
public class SynchronizedCache implements Cache {
// 需要被装饰的对象
private final Cache delegate;
// 将装饰对象被一个设置一个锁,为Cache添加了同步的功能
public SynchronizedCache(Cache delegate) {
this.delegate = delegate;
}
}
本篇详细说明了,Mybatis的缓存Cache以及为了拓展其功能使用的装饰器模式生成的其他不同组件,后续会对访问缓存进行详解。