mybatis有一级和二级两个缓存,在默认的情况下一级缓存是开启的,但是二级缓存是关闭的需要自己去配置,首先是在mybatis-config.xml
配置文件中打开二级缓存的总开关,然后就是在每个Mapper.xml文件中开启对应的二级缓存,因为二级缓存是基于命名空间,也可以变相的理解为基于一个Mapper.xml文件。在cache的标签里面对缓存的清理的策略有很多种,比如FIFO的先进先出策略,比如LRU的最少使用策略等。每种策略都对应一个类,如何创建这些类?还有mybatis还实现了阻塞队列版缓存,使用了BlockingCache装饰类。这么多功能如何糅合到一起?
1. Cache内层实现PerpetualCache
PerpetualCache
可以说是内层实现,也可以说的原始实现,他是实现Cache
接口,怎么说是内层实现呢。Cache
采用的是装饰器模式,装饰器类会对PerpetualCache
层层嵌套,装饰器的调用也是一层一层调用,最终调用到PerpetualCache
内的方法。
简单看其中几个方法的实现:
public class PerpetualCache implements Cache {
private final String id;
//缓存数据Map集合
private Map<Object, Object> cache = new HashMap<Object, Object>();
public PerpetualCache(String id) {
this.id = id;
}
//设置值
@Override
public void putObject(Object key, Object value) {
cache.put(key, value);//最终将键值添加到缓存中
}
//获取值
@Override
public Object getObject(Object key) {
return cache.get(key);
}
//…………
}
从上面的方法实现内容看来,就是简单的到cache中put放数据,get获取数据,但是这个简单的实现有很多问题,比如多线程数据安全问题,比如缓存不能清理,内存会被撑爆等。
2. 装饰器之LRU
LRU是mybatis的默认缓存回收策略,当达到LRU的设定大小的时候(默认大小1024,表示1024个key),会根据最小使用率的原则,将其移除出缓存。
看LruCache
的setSize
实现源码:
public void setSize(final int size) {
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;//判断当前keyMap的大小有没有超过最大限制尺寸
if (tooBig) {
eldestKey = eldest.getKey();//记录最少使用元素key
}
return tooBig;
}
};
}
keyMap
是LruCache
的成员变量,用来存储所有的key,在此方法中会去判断keyMap
中的key数量有没有达到上限,如果达到了上限,就将最少使用的元素key存储到另一个成员变量eldestKey
中,到这里setSize
方法结束。
putObject方法:
public void putObject(Object key, Object value) {
delegate.putObject(key, value);//调用下层装饰器的putObject方法
cycleKeyList(key);//循环键集合
}
首先是调用putObject
方法,会调用到下一个装饰器内putObject
实现方法,可能有很多层,但是最终还是会调用到PerpetualCache
内的putObject
方法。这里主要看的是cycleKeyList
方法,跟入进去。
cycleKeyList方法:
private void cycleKeyList(Object key) {
keyMap.put(key, key);//将键放入到keyMap集合中记录
if (eldestKey != null) {//判断是否有需要清理的键
delegate.removeObject(eldestKey);//调用下层装饰器的removeObject方法
eldestKey = null;//将eldestKey置空
}
}
这个方法很好理解,就是将键设置到keyMap
做记录存入,然后将setSize
中设置的eldestKey
交给下一层装饰器清理,层层调用,和上面的putObject
方式一样,最后还是会进入到PerpetualCache
内。
总结:由这里可以看的出来,在缓存的各种策略下,都是使用setSize
方法,判断当前的keyMap
中有没有达到设置的最大容量,然后根据不同的算法将这个key对应数据从cache中清理。锁定所要清理数据的过程是在setSize
中实现,记录最新的key,并且触发清理方法是在cycleKeyList
方法中实现,二级缓存中所有的装饰器都是这么实现的。回收策略的装饰器就看这一个,其他就是同理。
3. 阻塞队列BlockingCache
阻塞队列就是实行排队机制,来之前先拿锁,拿到锁的执行,未拿到锁的等待。拿到锁的线程先是从缓存中获取数据,如果没有,就去数据库中获取,获取以后会将数据缓存起来,并释放锁。以后的线程进来如果缓存中有数据就直接从缓存中获取,提高了效率。同时也解决了线程安全问题,多线程操作同一拨数据,造成数据的不一致等问题。
先看getObject方法:
public Object getObject(Object key) {
acquireLock(key);//尝试获取锁
Object value = delegate.getObject(key);//装饰器依次调用
if (value != null) {//获取到了值
releaseLock(key);//释放锁
}
return value;//返回结果
}
这个方法其实是有一个问题的,开始是尝试获取锁了,但是在释放锁的时候只在value不为空的时候做了,如果value不为空,那么这个锁会不会一直被持有不释放呢?
在缓存中,如果获取不到数据就会到数据库中获取,数据库中获取到的时候会被缓存起来,这里关键点就是将查库后的数据放到缓存中,会去调用putObject
方法,接下来看这个方法。
putObject方法:
public void putObject(Object key, Object value) {
try {
delegate.putObject(key, value);
} finally {
releaseLock(key);//释放锁
}
}
putObject
方法中只有释放锁的过程,并没有获取锁的过程。这个就和之前的getObject
方法不谋而合啦。其实value为空后,真正的释放锁的过程是在putObject
方法中。
总结:这个过程理解就在于putObject
和getObject
存在锁的获取、释放的关联。如果单独看一个方法,会觉得这是明显的错误,但是两个方法合起来看就知道设计的巧妙之处。下面展示一下Blocking实现的原理图(度娘一大堆,但是自己画出来的才是自己的):
4. 总结
虽然mybatis的二级缓存很少被用到,因为它存在一些弊端,比如数据的脏读。但是作为mybatis的一个组件还是要有一定的了解,可能这个功能用不上,但是里面的实现思路是很好的(设计模式、阻塞队列锁的获取和释放逻辑),可以学习并融入到自己的开发过程中。
##### 番外说明篇
曾经我觉得一篇文章写的越长越好,但是后来没事的时候我看自己之前写的长篇博客,真的有点看不下去,因为涉及到东西多,内容也太多,看着很不方便。所以现在写博客我都是尽量控制篇幅不要太长,如果涉及到其他知识点,我就会另外开一篇博客。
这一篇也不例外,这里二级缓存的设计使用到了装饰器模式,具体装饰器是如何实现这里没有做太多的说明,写起来就会有点多,也会让这片博客不纯粹。所以下一篇会详细说说装饰器模式(也可能是下下篇,也可能……)。