写在前面
聊一聊MyBatis是如何使用装饰者模式的,顺便回顾下缓存的相关知识,可以看看右侧目录一览内容概述。
装饰者模式
这里就不聊它的概念了,总结下就是套娃。利用组合的方式将装饰器组合进来,增强共同的抽象方法(与代理很类似但是又更灵活)
MyBatis缓存
回忆下传统手艺
<!-- 先进先出,60秒刷新一次,可存储512个引用,返回对象只读,不同线程中的调用者之间修改会导致冲突 -->
<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
粗略回顾下MyBatis缓存
一级缓存
MyBatis的一级缓存存在于SqlSession的生命周期中,在同一个SqlSession中查询时,MyBatis会把执行的方法和参数通过算法生成缓存的键值,将键值和查询结果存入一个Map对象中。如果同一个SqlSession中执行的方法和参数完全一致,那么通过算法会生成相同键值,当Map缓存对象中已经存在该键值时,则会返回缓存中的对象。
默认开启
二级缓存
MyBatis的二级缓存非常强大,它不同于一级缓存只存在于SqlSession的生命周期中,而是可以理解为存在于SqlSessionFactory的生命周期中。
默认不开启,需要如下配置后开启全局配置,再在对应的Mapper.xml中添加“传统手艺”-
<settings>
<setting name = "cacheEnabled" value="true"/>
</settings>
<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
另一种开启方式-注解
@CacheNamespace(
eviction = FifoCache.class,
flushInterval = 60000,
size = 512,
readWrite = true
)
public interface RoleMapper {
// 接口方法
}
- eviction(收回策略)
- LRU(最近最少使用的):移除长时间不使用的对象,这是默认值
- FIFO(先进先出):按对象进入缓存的顺序来移除它们
- SOFT(软引用):移除基于垃圾回收器状态和软引用规则的对象
- WEAK(弱引用):更积极地移除基于垃圾收集器状态和弱引用规则的对象
- flushInterval(刷新间隔)
- size(引用数目)
- readOnly(只读)只读的缓存会给所有调用者返回缓存的相同实例,因此这些对象不能被修改,这提供了很重要的性能优势。可读写的缓存会通过序列化返回缓存对象的拷贝,这种方式会慢一些,但是安全,因此默认是false
集成第三方缓存
MyBatis还支持通过“type”来集成第三方缓存,如下就是集成了Redis缓存,这样就从本地缓存跳跃到了分布式缓存了。
<mapper namespace="xxx.xxx.xxx.mapper.RoleMapper">
<!-- 集成Redis缓存-->
<cache type="org.mybatis.caches.redis.RedisCache" />
</mapper>
二级缓存的问题-脏数据
二级缓存虽然能提高应用效率,减轻数据库服务器的压力,但是如果使用不当,很容易产生脏数据
MyBatis的二级缓存是和命名空间绑定的,所以通常情况下每一个Mapper映射文件都拥有自己的二级缓存,不同Mapper的二级缓存互不影响。在常见的数据库操作中,多表联合查询非常常见,由于关系型数据库的设计,使得很多时候需要关联多个表才能获得想要的数据。在关联多表查询时肯定会将查询放到某个命名空间下的映射文件中,这样一个多表的查询就会缓存在该命名空间的二级缓存中。涉及这些表的增删改操作通常不在一个映射文件中,它们的命名空间不同,因此当有数据变化时,多表查询的缓存未必会被清空,这种情况下就会产生脏数据。
基于MyBatis缓存机制结合源码解析装饰器模式
Cache接口:
Cache核心方法:
- putObject
- getObject
- removeObject
DEMO-实战使用MyBatis的装饰者模式
public static void main(String[] args) {
final String cacheKey = "cache";
final Cache cache = new LoggingCache(new BlockingCache(new PerpetualCache(cacheKey)));
Object cacheValue = cache.getObject(cacheKey);
if (Objects.isNull(cacheValue)) {
log.debug("缓存未命中 >>>>>>>>> key:[{}]", cacheKey);
cache.putObject(cacheKey, "MyCacheValue");
}
cacheValue = cache.getObject(cacheKey);
log.debug("缓存命中 >>>>>>>>> key:[{}],value:[{}]", cacheKey, cacheValue);
}
如代码所示,是不是看到了“装饰者模式”的影子了,在构造函数中疯狂套娃。使用的是MyBatis的API,给基本缓存组件装饰了“日志打印”、“阻塞“的能力。
结果演示:
可以看到,LogginCache在读缓存的时候还会打印出缓存命中率。 好了,接下来进入正题,看看其他缓存是怎么实现的吧。以下源码基于MyBatis3.4.5
PerpetualCache
private final Map<Object, Object> cache = new HashMap<>();
@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);
}
这是MyBatis的基础缓存,套娃的基本得有它,它的核心就是个HashMap来作为缓存容器,其实现的Cache接口的几个核心方法也都是委托给了HashMap去做。
FifoCache
一个支持先进先出的缓存策略的MyBatisCache
private final Cache delegate;
//维护一个key的双端队列
private final Deque<Object> keyList;
private int size;
public FifoCache(Cache delegate) {
//通过构造函数,将Cache组合进来,取名”委托“
this.delegate = delegate;
this.keyList = new LinkedList<>();
this.size = 1024;
}
@Override
public void putObject(Object key, Object value) {
//先走自己的增强
cycleKeyList(key);
//真实的写缓存交给”委托“去做
delegate.putObject(key, value);
}
@Override
public Object getObject(Object key) {
return delegate.getObject(key);
}
@Override
public Object removeObject(Object key) {
return delegate.removeObject(key);
}
private void cycleKeyList(Object key) {
//将新写的缓存key添加到双端队列末尾
keyList.addLast(key);
// 如果key的大小大于了1024(构造函数中默认赋值1024)则会移除最早添加的缓存
// 1. 移除自身维护的key队列的队头 2.委托给“委托”去真实删除队头缓存对象
if (keyList.size() > size) {
Object oldestKey = keyList.removeFirst();
delegate.removeObject(oldestKey);
}
}
以上就是MyBatis先进先出缓存的实现了,FifoCache维护了key的双端队列,每次写缓存的时候会判断大小如果大于阈值则会先移除队头的key,再委托给组合进来的Cache来删除对应缓存操作,完成“先进先出”的增强(装饰)
LruCache
一个支持LRU(Least Recently Used ,最近最少使用)缓存策略的MyBatisCache
回忆下缓存策略
- LRU:Least Recently Used,最近最少使用
- LFU:Least Frequently Used,最近不常被使用
LRU 算法有一个缺点,比如说很久没有使用的一个键值,如果最近被访问了一次,那么即使它是使用次数最少的缓存,它也不会被淘汰;而 LFU 算法解决了偶尔被访问一次之后,数据就不会被淘汰的问题,它是根据总访问次数来淘汰数据的,其核心思想是“如果数据过去被访问多次,那么将来它被访问次数也会比较多”。因此 LFU 可以理解为比 LRU 更加合理的淘汰算法。
回忆下LinkedHashMap的核心机制-LRU
LinkedHashMap相比HashMap多了两个节点,before,after这样就能够维护节点之间的顺序了。
我们看看LinkedHashMap的get方法,它内部有LinkedHashMap开启LRU机制的秘密。
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder) // 为true则会执行afterNodeAccess(将节点移动到队尾)
afterNodeAccess(e);
return e.value;
}
void afterNodeAccess(Node<K,V> e) { // move node to last (官方注释 言简意赅 -> 将节点移动到队尾)
LinkedHashMap.Entry<K,V> last;
if (accessOrder && (last = tail) != e) {
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
那么这个accessOrder变量是怎么维护的呢?看代码
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
你会发现,LinkedHashMap有这么一个构造函数,第三个参数便是accessOrder,所以决定是否开启LRU是你在运行时传参决定的!开启后则会在每次读取键值对之后将读取的节点移动至队尾,那么队头就是最近最少使用的了,队尾就是刚刚使用的了,当需要删除最近最少使用的节点的时候,直接删除队头的即可。
回忆下LinkedHashMap的核心方法-removeEldestEntry
LinkedHashMap是一个有顺序的HashMap,它可以使得你的k,v能够按照某种顺序写入和读取,它的核心方法removeEldestEntry功不可没。
在HashMap新增k,v之后会回调一个方法“afterNodeInsertion”,这个方法在HashMap中是一个空实现(俗称钩子方法),它的子类LinkedHashMap重写了它,代码如下。
void afterNodeInsertion(boolean evict) { // possibly remove eldest 这是官方注释,言简意赅(可能会删除老key)
LinkedHashMap.Entry<K,V> first;
//前面的短路方法不管,我们关注removeEldestEntry方法 -> 如果该方法也返回true,则会走方法体中的removeNode方法(删除first节点的元素)。
// 当开启LinkedHashMap的LRU模式,则队头的元素是“最近最少使用的元素”,因为每次读取k,v后都会将元素调整至队尾,所以队头的元素是“最近最少使用的元素“
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
进入正题
private final Cache delegate;
// 维护一个key和value都是缓存key的map
private Map<Object, Object> keyMap;
//最近最少使用的Key
private Object eldestKey;
public LruCache(Cache delegate) {
//通过构造函数,将Cache组合进来,取名”委托“
this.delegate = delegate;
//初始化keyMap(重要)
setSize(1024);
}
public void setSize(final int size) {
// 构造函数第三个参数传递true(accessOrder),如上所述将开启LRU模式
keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
private static final long serialVersionUID = 4267176411845948333L;
// 重写了LinkedHashMap的方法
@Override
protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
boolean tooBig = size() > size;
if (tooBig) {
// 大小超过阈值,将队头(最近最少使用)的key更新至自身维护的"eldestKey" (重要)
eldestKey = eldest.getKey();
}
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);
}
private void cycleKeyList(Object key) {
// 因为重写了LinkedHashMap的removeEldestEntry方法,如上所述,超过阈值后eldestKey指向的就是最近最少使用的key
keyMap.put(key, key);
if (eldestKey != null) {
// 委托移除最近最少使用的缓存
delegate.removeObject(eldestKey);
// 置空
eldestKey = null;
}
}
以上就是MyBatis中的LRU缓存的机制了,自身维护了一个LinkedHashMap,开启了LRU机制,重写了removeEldestEntry方法,当大小触发阈值的时候维护最近最少使用的元素key,委托给组合进来的Cache对象移除,整个流程下来就使得被装饰着有了LRU的增强。
SoftCache
一个软引用的MyBatisCache
弱引用
弱引用比强引用稍弱一些。当JVM内存不足时,GC才会回收那些只被软引用指向的对象,从而避免OutOfMemoryError。当GC将只被软引用指向的对象全部回收之后,内存依然不足时,JVM才会抛出OutOfMemoryError。(这一特性非常适合做缓存,毕竟最终数据源在DB,还能保护JVM进程)
// 维护最近经常使用的缓存数据,该集合会使用强引用指向其中的每个缓存Value,防止被GC回收
private final Deque<Object> hardLinksToAvoidGarbageCollection;
//与SortEntry对象关联,用于记录已经被回收的缓存条目
private final ReferenceQueue<Object> queueOfGarbageCollectedEntries;
private final Cache delegate;
//强引用的个数,默认256。即有256个热点数据无法直接被GC回收
private int numberOfHardLinks;
public SoftCache(Cache delegate) {
this.delegate = delegate;
this.numberOfHardLinks = 256;
this.hardLinksToAvoidGarbageCollection = new LinkedList<Object>();
this.queueOfGarbageCollectedEntries = new ReferenceQueue<Object>();
}
@Override
public void putObject(Object key, Object value) {
// 同步删除已经被GC回收的Value
removeGarbageCollectedItems();
delegate.putObject(key, new SoftEntry(key, value, queueOfGarbageCollectedEntries));
}
private static class SoftEntry extends SoftReference<Object> {
private final Object key;
SoftEntry(Object key, Object value, ReferenceQueue<Object> garbageCollectionQueue) {
// 关联引用队列。
// 当SoftReference指向的对象被回收的时候,JVM就会将这个SoftReference作为通知,添加到与其关联的引用队列
super(value, garbageCollectionQueue);
this.key = key;
}
}
@Override
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();
if (result == null) {
// 重要的一步!判断Value是否为空,为空则表示弱引用指向的对象已经被GC回收了,就需要同步删除该缓存。
delegate.removeObject(key);
} else {
// See #586 (and #335) modifications need more than a read lock
// 读取缓存后,维护“强引用”的数据。
synchronized (hardLinksToAvoidGarbageCollection) {
hardLinksToAvoidGarbageCollection.addFirst(result); // 将缓存添加进强引用队列(热点数据)
if (hardLinksToAvoidGarbageCollection.size() > numberOfHardLinks) {
hardLinksToAvoidGarbageCollection.removeLast(); // 维护队列个数
}
}
}
}
return result;
}
@Override
public Object removeObject(Object key) {
removeGarbageCollectedItems(); // 删除被GC回收的Value
return delegate.removeObject(key); // 委托删除缓存
}
private void removeGarbageCollectedItems() {
SoftEntry sv;
// 引用关联的队列如果有值,则说明有被GC回收的Value
while ((sv = (SoftEntry) queueOfGarbageCollectedEntries.poll()) != null) {
delegate.removeObject(sv.key);
}
}
WeakCache
一个弱引用的MyBatisCache
与弱引用类似(基本相同),不过多介绍了。
弱引用
弱引用比软引用的引用强度还要弱。弱引用可以引用一个对象,但无法阻止这个对象被GC回收,也就是说,在JVM进行垃圾回收的时候,若发现某个对象只有一个弱引用指向它,那么这个对象会被GC立刻回收。(即遇GC比死,存活的时间为两次GC之间)
// Entry继承的是WeakReference。
// 其他内容参考弱引用Cache
private static class WeakEntry extends WeakReference<Object> {
private final Object key;
private WeakEntry(Object key, Object value, ReferenceQueue<Object> garbageCollectionQueue) {
super(value, garbageCollectionQueue);
this.key = key;
}
}
LoggingCache
一个支持打印Debug级别的缓存命中率的MyBatisCache
// 日志打印的log对象
private final Log log;
private final Cache delegate;
// 请求数
protected int requests = 0;
// 缓存命中数
protected int hits = 0;
public LoggingCache(Cache delegate) {
//通过构造函数,将Cache组合进来,取名”委托“
this.delegate = delegate;
//log通过缓存id作为表示
this.log = LogFactory.getLog(getId());
}
@Override
public void putObject(Object key, Object object) {
delegate.putObject(key, object);
}
@Override
public Object getObject(Object key) {
requests++; // 请求数增加
final Object value = delegate.getObject(key);
if (value != null) {
hits++; // 缓存命中,命中数增加
}
if (log.isDebugEnabled()) {
log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio()); // 打印缓存命中率
}
return value;
}
@Override
public Object removeObject(Object key) {
return delegate.removeObject(key);
}
private double getHitRatio() {
// 计算缓存命中率
return (double) hits / (double) requests;
}
LoggingCache使得缓存读取的时候能够有缓存命中率的日志打印,挺实用的增强。
BlockingCache
一个支持阻塞的MyBatisCache
private long timeout;
private final Cache delegate;
//每个key都有自己的ReentrantLock
private final ConcurrentHashMap<Object, ReentrantLock> locks;
public BlockingCache(Cache delegate) {
this.delegate = delegate;
this.locks = new ConcurrentHashMap<Object, ReentrantLock>();
}
@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;
}
private void acquireLock(Object key) {
Lock lock = getLockForKey(key); // 获取对应的Lock,没有则新增一把Lock
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 ReentrantLock getLockForKey(Object key) {
ReentrantLock lock = new ReentrantLock();
ReentrantLock previous = locks.putIfAbsent(key, lock);
return previous == null ? lock : previous;
}
private void releaseLock(Object key) {
ReentrantLock lock = locks.get(key); // 获取Key对应的Lock
if (lock.isHeldByCurrentThread()) { // 如果是当前线程持有lock,则释放锁
lock.unlock();
}
}
SynchronizedCache
一个支持同步的MyBatisCache,从名称就能知道实现原理是synchronized关键字
public SynchronizedCache(Cache delegate) {
this.delegate = delegate;
}
@Override
public synchronized int getSize() {
return delegate.getSize();
}
@Override
public synchronized void putObject(Object key, Object object) {
delegate.putObject(key, object);
}
@Override
public synchronized Object getObject(Object key) {
return delegate.getObject(key);
}
@Override
public synchronized Object removeObject(Object key) {
return delegate.removeObject(key);
}
同步缓存就是给核心方法加上了同步锁,保证了线程安全。
跟随源码看看解析-装饰过程
cacheElement方法解析cache标签
可以看出最底层是PerpetualCache,默认装饰的是LruCache。
如下就是将剩下的装饰器循环装饰的过程了,细节就不追进去了。
以上就是MyBatis对于缓存的装饰者设计模式的实践相关的源码简单追踪了。
跟随源码看看缓存的使用的地方
先随便点击Cache接口的一方法,看看在哪里有使用。很明显,那个BaseExecutor的类就是正儿八经使用的地方。
query方法中很明显表示了先从缓存中获取,如果没有则走DB(还会写缓存)
代码也很简单,就是从DB获取然后写入缓存
总结
笔者先简单描述了装饰者模式,随后回忆了MyBatis的缓存传统手艺-cache标签的使用,以及一级二级缓存,描述了集成第三方缓存(解决JVM缓存的单点问题)。
随后结合源码介绍了MyBatis的Cache接口及其相关的实现类,首先通过Demo言简意赅地表达了装饰者模式的使用以及MyBatisCache装饰者模式使用的效果(LoggingCache)
紧接着笔者介绍了
- PerpetualCache这个最关键最核心的缓存实现类,它的核心是一个HashMap;
- FifoCache先进先出淘汰策略的缓存实现类,它的核心是一个维护key的双端队列,添加缓存前先维护这个双端队列,如果size到达阈值则移除队头的元素;
- LruCache最近最少使用淘汰策略的缓存实现类,它的核心是基于LinkedHashMap实现LRU机制,我们也回忆了LRU以及LinkedHashMap相关的知识点,其关键点就是一个继承了LinkedHashMap的keyMap(KV都是缓存Key),重写了LinkedHashMap的重要方法removeEldestEntry,用于记录最近最少使用的key,在适当时机删除该缓存;
- SoftCache、WeakCache我们回忆了软引用、弱引用的相关知识,其核心就是对应的Value组件Entry继承了SoftReference、WeakReference;
- BlockingCache这个阻塞缓存的核心就是大名鼎鼎的ReentrantLock;
- SynchronizedCache这个缓存顾名思义就是核心方法追加了synchronized的关键字,事实也确实如此。
为什么要使用缓存?走DB的链路上层用缓存抗一抗再正常不过了。 为什么用装饰者模式?这个场景它的核心就是缓存策略有很多,它们互相可以叠加,可以在配置的时候灵活配置,那么就可以通过解析配置后在运行时灵活的“装饰”起来,达到最后的预期效果,挺妙的。
关于多种Cache的核心实现,以及相关的周边技术可以反复琢磨,比如锁的使用、缓存的读写、LinkedHashMap、JVM的GC等等,毕竟这是开源框架的实战代码,这些都是值得我们像骆驼一样反复咀嚼,反复反刍的,至少了解了这一块,后续你真的有类似实战的时候之前可以先参考参考了!
好了,以上就是MyBatis缓存解析-装饰者设计模式了。欢迎多多交流,希望对你有帮助。原创不易..(没想到这么难,本来想总结下,发现一两次还写不完,光扣字都扣傻了 哈哈..)