为啥使用缓存?
这是一个值得思考清楚的问题, 为了能够加快访问速度,提高系统的吞吐量。
使用缓存带来的问题?
缓存一致性问题, 即缓存是否与最新的数据保持一致。
mybatis的缓存
mybatis作为一个ORM框架, 数据的通过JDBC从数据库读取, 读取是需要进行网络通信IO,以及磁盘IO,为了提升速度,势必有必要进行缓存到内存。就比如查询操作, 如果是对同一个缓存key进行操作, 那就没必要每次都去查数据库, 直接走缓存。
mybatis的缓存设计体系
mybatis的缓存分为一级缓存和二级缓存。 访问的时候先走二级缓存, 在走一级缓存。 一级缓存一直存在, 二级缓存默认开启, 但是需要给MapperStatement设置开启标识。
mybatis的缓存接口如下
public interface Cache {
/**
* @return The identifier of this cache
*/
String getId();
/**
* @param key
* Can be any object but usually it is a {@link CacheKey}
* @param value
* The result of a select.
*/
void putObject(Object key, Object value);
/**
* @param key
* The key
* @return The object stored in the cache.
*/
Object getObject(Object key);
/**
* As of 3.3.0 this method is only called during a rollback
* for any previous value that was missing in the cache.
* This lets any blocking cache to release the lock that
* may have previously put on the key.
* A blocking cache puts a lock when a value is null
* and releases it when the value is back again.
* This way other threads will wait for the value to be
* available instead of hitting the database.
*
*
* @param key
* The key
* @return Not used
*/
Object removeObject(Object key);
/**
* Clears this cache instance.
*/
void clear();
/**
* Optional. This method is not called by the core.
*
* @return The number of elements stored in the cache (not its capacity).
*/
int getSize();
/**
* Optional. As of 3.2.6 this method is no longer called by the core.
* <p>
* Any locking needed by the cache must be provided internally by the cache provider.
*
* @return A ReadWriteLock
*/
default ReadWriteLock getReadWriteLock() {
return null;
}
}
缓存接口实现类分析
首先得知道mybatis的缓存设置整体上使用了装饰器设计模式 也有委托者模式的影子。
PerpetualCache
缓存说到底就是把数据存储在内存里面, 那么必定会有一个数据结构用来存储的,所以这个PerpetualCache 既是缓存功能实现的基石。
public class PerpetualCache implements Cache { //持久化缓存
private final String 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()); //使用id进行判断
}
@Override
public int hashCode() {
if (getId() == null) {
throw new CacheException("Cache instances require an ID.");
}
return getId().hashCode();
}
}
关键也就是使用了HashMap作为缓存的存储结构。
LruCache
缓存, 如果没有置换算法,时不能一直往里面加的,比如我设置缓存大小为10M, 那么 但要达到10M,就必须采取换粗置换算法,把一些不值得的数据用新的覆盖掉, LruCache采用了最近最久未使用的算来淘汰, 当然还有一个比较出名的最近最少使用。 Mybatis默认提供了前者。 大家也可以想一下, 用户什么数据结构实现比较好。链表。
为啥呢, 想一下缓存key一直put, 如果使用数组来维护, 数据就得一直扩容,所以使用链表比较号,不管时使用数据还是使用链表, 一个问题就遍历查找效率都比较低, 所以需要使用hash表来提升查找速度,这样我们就可以想到HashMap, 但是我们还需要记录添加的先后顺序,所以想到LinkedHashMap。
熟悉HashMap代码的, 应该都会留意HashMap的插入过程留下很多的模板方法,提供一些功能扩展。
LinkedHashMap, 最大的特点就是说期维护了一个前后插入关系的链表结构,要知道HashMap的读取的时候是和插入的时候的顺序无关。
那LinkedHashMap怎么做到呢?
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after; //LinkedhashMap为啥有序的原因
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
上图为LinkedHashMap的Entry节点, 其继承了HashMap的Node节点。
补充了 before, after 引用, 这也就说明了LinkedHashMap如何做到有序的。
另一个问题就在那些地方维护这样一个before、after结构呢?
在HashMap的put过程中, 主要是两种情况, 第一种就是put的元素key在HashMap不存在,属于新插入的,所以在这种情况下 Before、After的结构 采用尾巴插法,维护先后关系。 第二种情况下就是 插入的key在HashMap中是存在的,根据HashMap的代码扩展模板方法, 可以看见LinkedHashMap在这种情况下是在afterNodeAccess(Node)方法中维护了 这个Before、After关系。
前面说了这么多, 那这和LRU有啥关系呢?
既然数据已经存入了,那么现在只需要把最久为使用的节点拿掉就行。
这部分逻辑在模板方法afterNodeInsertion(evict)中实现。
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
从图中可见first指向了 最久为使用的head节点。
其中LinkedHashMap又提供了一个removeEldestEntry方法留给子类去扩展,到底要不要移除这个最久为使用的节点。
最后回到LruCache , keyMap实现了LinkedHashMap 并重写removeEldestEntry方法, 设置了如果LinkedHashMap的数据量大小大于设置的size,那么久进行移除。 并记录这要移除的key,用来移除缓存。
private void cycleKeyList(Object key) {
keyMap.put(key, key); //lru
if (eldestKey != null) {//存不存在老key
delegate.removeObject(eldestKey);//缓存移除
eldestKey = null;
}
}
BlockingCache
当在缓存中找不到元素时,它在缓存键上设置一个锁。这样,其他线程将等待该元素被填满,而不是访问数据库。根据其性质,如果使用不当,此实现可能导致死锁。
public class BlockingCache implements Cache {
private long timeout;
private final Cache delegate;
private final ConcurrentHashMap<Object, CountDownLatch> locks;//呃,这个也太离谱了
}
其数据结构上述代码所示, timeout 为等待锁的超时时间, delegate为委托的另外缓存,locks 为map结构key为缓存key ,value为锁对象, 有趣的是这个锁对象使用的是CountDownLatch,
添加缓存
添加缓存, 并不控制,只能由一个线程去给当前key添加, 当缓存添加之后需要释放锁,缓存所有等待该缓存的线程。
private void releaseLock(Object key) {
CountDownLatch latch = locks.remove(key);//获取该缓存key的 倒计数器
if (latch == null) { //这种情况不应该发生
throw new IllegalStateException("Detected an attempt at releasing unacquired lock. This should never happen.");
}
latch.countDown(); //CountDownLatch 用法
}
获取缓存
public Object getObject(Object key) {
acquireLock(key);//尝试获取锁
Object value = delegate.getObject(key);
if (value != null) {
releaseLock(key); //释放锁
}
return value;
}
获取锁的方法
private void acquireLock(Object key) {
CountDownLatch newLatch = new CountDownLatch(1); //创建一个倒计数器
while (true) {
CountDownLatch latch = locks.putIfAbsent(key, newLatch); //如果key不存在那么我就把当前的到计数器塞入 并发map, 存在那么返回原先的到计数器
if (latch == null) { //说明我是第一个访问该key的
break;//那我就不用阻塞
}
try {
if (timeout > 0) { //设置了超时时间
boolean acquired = latch.await(timeout, TimeUnit.MILLISECONDS); //等待
if (!acquired) { //没有获取锁
throw new CacheException(
"Couldn't get a lock in " + timeout + " for the key " + key + " at the cache " + delegate.getId());
}
} else {
latch.await(); //一直阻塞
}
} catch (InterruptedException e) { //中断异常
throw new CacheException("Got interrupted while trying to acquire lock for key " + key, e);
}
}
}
上述逻辑 所有的缓存key都会公用一个CounDownLatch, 并由添加该缓存key是countDown释放锁, 但是我是感觉有个bug的就是, 第一个该key的缓存时不会被锁住的。
LoggingCache
这个就比较简单了, 一个日志缓存命中的装饰器。
数据结构为:
public class LoggingCache implements Cache { //日志缓存
private final Log log; //日志
private final Cache delegate;
protected int requests = 0; //请求次数
protected int hits = 0; //命中次数
}
具体计算命中百分比:
@Override
public Object getObject(Object key) {
requests++; //请求次数加1
final Object value = delegate.getObject(key);
if (value != null) {
hits++; //命中次数加一
}
if (log.isDebugEnabled()) { //输出日志
log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio());
}
return value;
}
FifoCache
先进先出的缓存装饰器
既然时先进先出, 那么一个比较合适的数据结构就是队列。
所以FIfoCache的数据结构如下:
public class FifoCache implements Cache {
private final Cache delegate;
private final Deque<Object> keyList; //队列数据结构
private int size; //队列大小
}
缓存淘汰算法关键代码:
private void cycleKeyList(Object key) {
keyList.addLast(key); //队列中为末尾添加缓存key
if (keyList.size() > size) { //如果达到了大小限制
Object oldestKey = keyList.removeFirst(); //移除最先进入的换粗key
delegate.removeObject(oldestKey); //移除缓存
}
}
整个装饰器并没有考虑线程安全问题, 所以必须来一个线程安全的装饰器。
SynchronizedCache
整个缓存装饰器, 的所有cache接口方法,在实现的时候都加了Synchronized。
ScheduledCache
这个装饰也很简单, 就是设置一个清理的间隔, 每次调用自己的方法都会去判断到了清理的时间间隔了没有,需要清理就会调用委托缓存的清理方法。
public class ScheduledCache implements Cache { //定时清理缓存
private final Cache delegate;
protected long clearInterval;
protected long lastClear;
}
private boolean clearWhenStale() {
if (System.currentTimeMillis() - lastClear > clearInterval) {//判断是否到了清理周期
clear();
return true;
}
return false;
}
SerializedCache
序列换缓存value的装饰器
添加缓存
public void putObject(Object key, Object object) {
if (object == null || object instanceof Serializable) { //判断value是否支持缓存
delegate.putObject(key, serialize((Serializable) object));
} else {
throw new CacheException("SharedCache failed to make a copy of a non-serializable object: " + object);
}
}
序列化方法
private byte[] serialize(Serializable value) {
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos)) {
oos.writeObject(value);
oos.flush();
return bos.toByteArray();
} catch (Exception e) {
throw new CacheException("Error serializing object. Cause: " + e, e);
}
}
反序列化方法
private Serializable deserialize(byte[] value) {
SerialFilterChecker.check();
Serializable result;
try (ByteArrayInputStream bis = new ByteArrayInputStream(value);
ObjectInputStream ois = new CustomObjectInputStream(bis)) {
result = (Serializable) ois.readObject();
} catch (Exception e) {
throw new CacheException("Error deserializing object. Cause: " + e, e);
}
return result;
}
WeakCache
弱引用的缓存,当JVM进行垃圾回收的时候就会把弱引用进行垃圾回收。
数据结构:
public class WeakCache implements Cache {
private final Deque<Object> hardLinksToAvoidGarbageCollection; //强引用避免垃圾回收
private final ReferenceQueue<Object> queueOfGarbageCollectedEntries; //引用队列
private final Cache delegate;
private int numberOfHardLinks; //强引用数量限制
}
进行putObject
@Override
public void putObject(Object key, Object value) {
removeGarbageCollectedItems(); //清理弱引用队列里面的元素
delegate.putObject(key, new WeakEntry(key, value, queueOfGarbageCollectedEntries)); //添加弱引用, 并指定弱引用队列
}
getObject
@Override
public Object getObject(Object key) {
Object result = null;
@SuppressWarnings("unchecked") // assumed delegate cache is totally managed by this cache
WeakReference<Object> weakReference = (WeakReference<Object>) delegate.getObject(key);
if (weakReference != null) {
result = weakReference.get(); //说明已经进行一次垃圾回收, 已经放入引用队列
if (result == null) {
delegate.removeObject(key); //移除
} else { //次缓存被外交获取, 应该是比较重要的缓存,故添加强引用,避免垃圾回收
synchronized (hardLinksToAvoidGarbageCollection) {
hardLinksToAvoidGarbageCollection.addFirst(result);
if (hardLinksToAvoidGarbageCollection.size() > numberOfHardLinks) { //FIFO 缓存淘汰
hardLinksToAvoidGarbageCollection.removeLast();
}
}
}
}
return result;
}
SoftCache
软引用缓存装饰器 , 和弱引用装饰器差不多,区别就是在于,软引用时FULLGC时候才会把引用对象移动到引用队列。
总结
简要的介绍了 , Mybatis缓存组件, 我们可以自己实现缓存Cache接口, 然后让Mybatis使用我们的缓存实现。