LruCache最近最少使用的回收策略:
package org.apache.ibatis.cache.decorators;import java.util.LinkedHashMap;import java.util.Map;import java.util.concurrent.locks.ReadWriteLock;import org.apache.ibatis.cache.Cache;/** * 基于最近最少使用算法的回收策略 * @author easy288 * */public class LruCache implements Cache { // 代理的缓存对象 private final Cache delegate; // 此Map的key和value都是要添加的键值对的键 private Map
LruCache内部维护一个Map,
private MapkeyMap;
它的实际类型是LinkedHashMap<Object,Object>的匿名子类,子类重写了removeEldesEntry方法,用于获取在达到容量限制时被删除的key。
public void setSize(final int size) { keyMap = new LinkedHashMap(size, .75F, true) { private static final long serialVersionUID = 4267176411845948333L; protected boolean removeEldestEntry(Map.Entry eldest) { boolean tooBig = size() > size; if (tooBig) { eldestKey = eldest.getKey(); } return tooBig; } }; }
在每次调用setSize方法时,都会创建一个新的该类型的对象,同时指定其容量大小。第三个参数为true代表Map中的键值对列表要按照访问顺序排序,每次被方位的键值对都会被移动到列表尾部(值为false时按照插入顺序排序)。
LruCache只是修改了缓存的添加方式,
public void putObject(Object key, Object value) { delegate.putObject(key, value); cycleKeyList(key); }
在每次给代理缓存对象添加完键值对后,都会调用cycleKeyList方法进行一次检查。
/** * 循环keyList * @param key */ private void cycleKeyList(Object key) { // 把刚刚给代理缓存对象中添加的key,同时添加到keyMap中 keyMap.put(key, key); // 如果eldestKey不为null,则代表keyMap内部删除了eldestKey这个key if (eldestKey != null) { // 同样把代理缓存对象中key为eldestKey的键值对删除即可 delegate.removeObject(eldestKey); eldestKey = null; } }
LruCache把新添加的键值对的键添加到keyMap中,如果发现keyMap内部删除了一个key,则同样把代理缓存对象中相同的key删除。
LruCache就是以这种方式实现最近最少访问回收算法的。
ScheduledCache调度缓存装饰器:
它的内部维护2个字段,clearInterval和lastClear
// 调用clear()清空缓存的时间间隔,单位毫秒,默认1小时 protected long clearInterval; // 最后一次清空缓存的时间,单位毫秒 protected long lastClear;
在对代理的缓存对象进行任何操作之前,都会首先调用clearWhenStale()方法检查当前时间点是否需要清理一次缓存,如果需要则进行清理并返回true,否则返回false。
/** * 当缓存过期时调用clear方法进行清空 * @return 如果成功进行了清理则返回true,否则返回false */ private boolean clearWhenStale() { // 如果当前时间-最后一次清空时间>指定的时间间隔,则调用clear()进行清空 if (System.currentTimeMillis() - lastClear > clearInterval) { clear(); return true; } return false; }
SerializedCache序列化缓存装饰器:
它负责在调用putObject(key,value)方法保存key/value时,把value序列化为字节数组;在调用getObject(key)获取value时,把获取到了字节数组再反序列化回来。
使用此装饰器的前提是:所有要缓存的value必须实现Serializable接口,否则会抛出CacheException异常。
@Override public void putObject(Object key, Object object) { if (object == null || object instanceof Serializable) { delegate.putObject(key, serialize((Serializable) object)); } else { throw new CacheException("SharedCache failed to make a copy of a non-serializable object: " + object); } } @Override public Object getObject(Object key) { Object object = delegate.getObject(key); return object == null ? null : deserialize((byte[]) object); }
在反序列化时,SerializedCache使用了内部定义的类CustomObjectInputStream,此类继承自ObjectInputStream,重写了父类的resolveClass方法,区别在于解析加载Class对象时使用的ClassLoader类加载器不同。
public static class CustomObjectInputStream extends ObjectInputStream { public CustomObjectInputStream(InputStream in) throws IOException { super(in); } @Override protected Class resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { return Resources.classForName(desc.getName()); } }
LoggingCache日志缓存装饰器:
它内部维护2个字段:requests查询次数计数器和hits查询命中次数计数器
// 日志记录器 private Log log; // 代理的缓存对象 private Cache delegate; // 每次调用getObject(key)查询时,此值+1 protected int requests = 0; // 每次调用getObject(key)获取到的value不为null时,此值+1 protected int hits = 0;
在每次调用getObject方法从缓存对象中查询值时,都会迭代这两个计数器,并且计算实时命中率,打印到日志中。
@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; }
/** * 计算实时命中率 * @return */ private double getHitRatio() { return (double) hits / (double) requests; }
SynchronizedCache同步的缓存装饰器:
这个装饰器比较简单,只是把所有操作缓存对象的方法上都加了synchronized,用来避免多线程并发访问。
@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); } @Override public synchronized void clear() { delegate.clear(); }
FifoCache先进先出缓存回收策略装饰器:
它内部维护一个不限容量的LinkedList,名称为keyList,在构造方法中被创建。
private LinkedListkeyList;
public FifoCache(Cache delegate) { this.delegate = delegate; this.keyList = new LinkedList(); this.size = 1024; }
在调用putObject添加缓存时,会在向代理的缓存对象中添加数据之前,调用cycleKeyList进行一次验证,如果keyList超过限制长度,则进行回收。
@Override public void putObject(Object key, Object value) { cycleKeyList(key); delegate.putObject(key, value); }
private void cycleKeyList(Object key) { // 把key添加到keyList中 keyList.addLast(key); // 如果keyList超长,则移除第一个key,并获取被移除的key // 之后从代理缓存对象中,删除这个key if (keyList.size() > size) { Object oldestKey = keyList.removeFirst(); delegate.removeObject(oldestKey); } }
SoftCache软引用缓存装饰器:
它利用JDK的SoftReference软引用,借助垃圾回收器进行缓存对象的回收。
我们通常使用的引用方式都是强引用,如:Object obj = new Object();只要引用变量obj不为null,那么new出来的Object对象永远不会被垃圾回收器回收。而软引用的引用方式是这样的:
SoftReference ref = new SoftReference(new Object());
引用变量ref引用SoftReference对象(这属于强引用),再由SoftReference 对象内部引用new出来的Object对象(这属于软引用)。
自JDK1.2以后引入了java.lang.ref包,其中包含SoftReference(软引用)、WeakReference(弱引用)和PhantomReference(虚引用)三个引用类,引用类的主要功能就是实现被引用的对象仍可以被垃圾回收器回收这样一个功能。具体关于引用类的详解请点击
SoftCache数据结构如下:
// 强引用集合,最近一此查询命中的对象,其引用会被加入此集合的头部 // 集合采取先进先出策略,当长度超出指定size时,删除尾部元素 private final LinkedListhardLinksToAvoidGarbageCollection; // 此队列保存被垃圾回收器回收的对象所在的Reference对象 // 垃圾回收器在进行内存回收时,会把Reference对象内的引用变量置为null,同时将Reference对象加入队列中 private final ReferenceQueue queueOfGarbageCollectedEntries; // 代理的缓存对象 private final Cache delegate; // 强引用集合长度限制,可通过setSize方法设置,默认为256 private int numberOfHardLinks;
SoftCache在写缓存之前,会先调用removeGarbageCollectedItems()方法删除已经被垃圾回收器回收的key/value,之后想缓存对象中写入SoftEntry类型的对象(定义在SoftCache的内部,是SoftReference类的子类)。
@Override public void putObject(Object key, Object value) { removeGarbageCollectedItems(); delegate.putObject(key, new SoftEntry(key, value, queueOfGarbageCollectedEntries)); }
/** * 从缓存对象中删除已经被垃圾回收器回收的value对象对应的key */ private void removeGarbageCollectedItems() { SoftEntry sv; // 弹出队列中的所有key,依次删除 while ((sv = (SoftEntry) queueOfGarbageCollectedEntries.poll()) != null) { delegate.removeObject(sv.key); } }
SoftCache在读缓存时,是直接读取的,这样存在一个问题:缓存的value对象已经被垃圾回收器回收,但是该对象的软引用对象还存在,这种情况下要删除缓存对象中,软引用对象对应的key。另外,每次调用getObject查询到缓存对象中的value还未被回收时,都会把此对象的引用临时加入强引用集合,这样确保该对象不会被回收。这种机制保证了访问频次越搞的value对象,被回收的几率越小。
@Override public Object getObject(Object key) { Object result = null; @SuppressWarnings("unchecked") // assumed delegate cache is totally // managed by this cache SoftReferencesoftReference = (SoftReference ) delegate.getObject(key); // 引用变量为null,说明不存在key指定的键值对 if (softReference != null) { // 键值对存在的情况下,获取软引用变量引用的对象 result = softReference.get(); // 如果引用的value已经被回收,则删除缓存对象中的key if (result == null) { delegate.removeObject(key); } else { // See #586 (and #335) modifications need more than a read lock // 缓存的value对象没有被回收,且这次访问到了,则把此对象引用加入强引用集合 // 使其不会被回收 synchronized (hardLinksToAvoidGarbageCollection) { hardLinksToAvoidGarbageCollection.addFirst(result); if (hardLinksToAvoidGarbageCollection.size() > numberOfHardLinks) { hardLinksToAvoidGarbageCollection.removeLast(); } } } } return result; }
WeakCache弱引用缓存装饰器:
WeakCache在实现上与SoftCache几乎相同,只是把引用对象由SoftReference软引用换成了WeakReference弱引用。
最后,介绍特殊的缓存装饰器——TransactionalCache事务性缓存
TransactionalCache比其他Cache对象多出了2个方法:commit()和rollback()。TransactionalCache对象内部存在暂存区,所有对缓存对象的写操作都不会直接作用于缓存对象,而是被保存在暂存区,只有调用TransactionalCache的commit()方法时,所有的更新操作才会真正同步到缓存对象中。
这样的话,就会存在一个问题:
如果事务被设置为自动提交(autoCommit=true)的话,写操作会更新RDBMS(关系型数据库管理系统),但不会清空缓存对象(因为自动提交不会调用commit方法),这样会产生数据库与缓存中数据不一致的情况。如果缓存没有过期失效的机制,那么问题会很严重。
TransactionalCache数据结构如下:
// 原始的被代理的Cache缓存对象 private Cache delegate; // 调用commit()方法时是否清空缓存,初始为false // 如果此值为true,则调用commit时会进行清空缓存的操作 // 只有事务中包含更新操作时,此值才会为true // 否则只需要覆盖指定key/value的更新即可,(覆盖分为删除和添加两步操作) private boolean clearOnCommit; // 在commit时需要进行的添加操作 // 调用putObject方法时添加到这里 private MapentriesToAddOnCommit; // 在commit时需要进行的移除操作, // 调用removeObject时添加到这里 private Map entriesToRemoveOnCommit;
之所以把putObject操作分为删除和添加两步,我想可能是因为有的缓存的添加逻辑是:如果key已存在,则不允许添加,抛出异常。
到这里,mybatis内部Cache接口及其所有实现类都已解析完毕。
缓存体系中还有另外3个组件,分别是CacheKey、TransactionalCacheManager和CacheBuilder,会在后续陆续介绍。