Cache缓存框架源码阅读笔记

Scroll Down

Cache框架

该Cache框架源于hutool。该缓存属于本地缓存。

Cache接口

Cache接口定义了缓存的基本操作。
image.png
代码如下:

/**
 * 返回缓存容量,<code>0</code>表示无大小限制
 *
 * @return 返回缓存容量,<code>0</code>表示无大小限制
 */
int capacity();

/**
 * 缓存失效时长, <code>0</code> 表示没有设置,单位毫秒
 *
 * @return 缓存失效时长, <code>0</code> 表示没有设置,单位毫秒
 */
long timeout();

/**
 * 将对象加入到缓存,使用默认失效时长
 *
 * @param key    键
 * @param object 缓存的对象
 * @see Cache#put(Object, Object, long)
 */
void put(K key, V object);

/**
 * 将对象加入到缓存,使用指定失效时长<br>
 * 如果缓存空间满了,{@link #prune()} 将被调用以获得空间来存放新对象
 *
 * @param key     键
 * @param object  缓存的对象
 * @param timeout 失效时长,单位毫秒
 * @see Cache#put(Object, Object, long)
 */
void put(K key, V object, long timeout);

/**
 * 从缓存中获得对象,当对象不在缓存中或已经过期返回<code>null</code>
 * <p>
 * 调用此方法时,会检查上次调用时间,如果与当前时间差值大于超时时间返回<code>null</code>,否则返回值。
 * <p>
 * 每次调用此方法会刷新最后访问时间,也就是说会重新计算超时时间。
 *
 * @param key 键
 * @return 键对应的对象
 * @see #get(Object, boolean)
 */
default V get(K key) {
    return get(key, true);
}

/**
 * 从缓存中获得对象,当对象不在缓存中或已经过期返回Func0回调产生的对象
 * <p>
 * 调用此方法时,会检查上次调用时间,如果与当前时间差值大于超时时间返回<code>null</code>,否则返回值。
 * <p>
 * 每次调用此方法会刷新最后访问时间,也就是说会重新计算超时时间。
 *
 * @param key      键
 * @param supplier 如果不存在回调方法,用于生产值对象
 * @return 值对象
 */
default V get(K key, Func0<V> supplier) {
    return get(key, true, supplier);
}

/**
 * 从缓存中获得对象,当对象不在缓存中或已经过期返回Func0回调产生的对象
 * <p>
 * 调用此方法时,会检查上次调用时间,如果与当前时间差值大于超时时间返回<code>null</code>,否则返回值。
 * <p>
 * 每次调用此方法会刷新最后访问时间,也就是说会重新计算超时时间。
 *
 * @param key                键
 * @param isUpdateLastAccess 是否更新最后访问时间,即重新计算超时时间。
 * @param supplier           如果不存在回调方法,用于生产值对象
 * @return 值对象
 */
V get(K key, boolean isUpdateLastAccess, Func0<V> supplier);

/**
 * 从缓存中获得对象,当对象不在缓存中或已经过期返回<code>null</code>
 * <p>
 * 调用此方法时,会检查上次调用时间,如果与当前时间差值大于超时时间返回<code>null</code>,否则返回值。
 *
 * @param key                键
 * @param isUpdateLastAccess 是否更新最后访问时间,即重新计算超时时间。
 * @return 键对应的对象
 */
V get(K key, boolean isUpdateLastAccess);

/**
 * 返回包含键和值得迭代器
 *
 * @return 缓存对象迭代器
 * @since 4.0.10
 */
Iterator<CacheObj<K, V>> cacheObjIterator();

/**
 * 从缓存中清理过期对象,清理策略取决于具体实现
 *
 * @return 清理的缓存对象个数
 */
int prune();

/**
 * 缓存是否已满,仅用于有空间限制的缓存对象
 *
 * @return 缓存是否已满,仅用于有空间限制的缓存对象
 */
boolean isFull();

/**
 * 从缓存中移除对象
 *
 * @param key 键
 */
void remove(K key);

/**
 * 清空缓存
 */
void clear();

/**
 * 缓存的对象数量
 *
 * @return 缓存的对象数量
 */
int size();

/**
 * 缓存是否为空
 *
 * @return 缓存是否为空
 */
boolean isEmpty();

/**
 * 是否包含key
 *
 * @param key KEY
 * @return 是否包含key
 */
boolean containsKey(K key);

Cache接口的基本实现是在AbstractCache中实现的。基本类结构如下:
image.png
先来看下AbstractCache中的字段

protected Map<K, CacheObj<K, V>> cacheMap;

private final StampedLock lock = new StampedLock();

/**
 * 返回缓存容量,<code>0</code>表示无大小限制
 */
protected int capacity;
/**
 * 缓存失效时长, <code>0</code> 表示无限制,单位毫秒
 */
protected long timeout;

/**
 * 每个对象是否有单独的失效时长,用于决定清理过期对象是否有必要。
 */
protected boolean existCustomTimeout;

/**
 * 命中数
 */
protected int hitCount;
/**
 * 丢失数
 */
protected int missCount;

put元素

put元素的逻辑是首先标记元素是否存在超时时间,然后判断Map是否已经存满,如果满了则清理缓存。
然后放入元素。其中清理过期对象是抽象函数,子类会根据不同的需求实现清理逻辑。

@Override
public void put(K key, V object) {
    put(key, object, timeout);
}

@Override
public void put(K key, V object, long timeout) {
    final long stamp = lock.writeLock();
    try {
        putWithoutLock(key, object, timeout);
    } finally {
        lock.unlockWrite(stamp);
    }
}

/**
 * 加入元素,无锁
 *
 * @param key     键
 * @param object  值
 * @param timeout 超时时长
 * @since 4.5.16
 */
private void putWithoutLock(K key, V object, long timeout) {
    CacheObj<K, V> co = new CacheObj<>(key, object, timeout);
    if (timeout != 0) {
        existCustomTimeout = true;
    }
    if (isFull()) {
        pruneCache();
    }
    cacheMap.put(key, co);
}

get元素

获取元素如下:
这里主要说明下get元素的逻辑:
首先获取乐观锁,从Map中获取元素,如果返回元素为空则记录缓存未命中次数。接下来判断元素是否失效,如果实现则记录缓存未命中次数,并清理该元素。如果未失效则记录缓存命中次数,并返回数据。
如果第一次尝试没有获取到元素,则通过写锁再次获取Map中的元素。如果元素为空或者元素已经过期则执行回调函数返回结果。然后将结果放入Map中。

@Override
public boolean containsKey(K key) {
    final long stamp = lock.readLock();
    try {
        // 不存在或已移除
        final CacheObj<K, V> co = cacheMap.get(key);
        if (co == null) {
            return false;
        }

        if (false == co.isExpired()) {
            // 命中
            return true;
        }
    } finally {
        lock.unlockRead(stamp);
    }

    // 过期
    remove(key, true);
    return false;
}

/**
 * @return 命中数
 */
public int getHitCount() {
    return hitCount;
}

/**
 * @return 丢失数
 */
public int getMissCount() {
    return missCount;
}

@Override
public V get(K key, boolean isUpdateLastAccess, Func0<V> supplier) {
    V v = get(key, isUpdateLastAccess);
    if (null == v && null != supplier) {
        final long stamp = lock.writeLock();
        try {
            // 双重检查锁
            final CacheObj<K, V> co = cacheMap.get(key);
            if (null == co || co.isExpired()) {
                try {
                    v = supplier.call();
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
                putWithoutLock(key, v, this.timeout);
            } else {
                v = co.get(true);
            }
        } finally {
            lock.unlockWrite(stamp);
        }
    }
    return v;
}

@Override
public V get(K key, boolean isUpdateLastAccess) {
    // 尝试读取缓存,使用乐观读锁
    long stamp = lock.readLock();
    try {
        // 不存在或已移除
        final CacheObj<K, V> co = cacheMap.get(key);
        if (null == co) {
            missCount++;
            return null;
        }

        if (co.isExpired()) {
            missCount++;
        } else{
            // 命中
            hitCount++;
            return co.get(isUpdateLastAccess);
        }
    } finally {
        lock.unlock(stamp);
    }

    // 过期
    remove(key, true);
    return null;
}

AbstractCache的实现类

FIFOCache先入先出缓存

注释说明了该实现类的功能

/**
 * FIFO(first in first out) 先进先出缓存.
 *
 * <p>
 * 元素不停的加入缓存直到缓存满为止,当缓存满时,清理过期缓存对象,清理后依旧满则删除先入的缓存(链表首部对象)<br>
 * 优点:简单快速 <br>
 * 缺点:不灵活,不能保证最常用的对象总是被保留
 * </p>
 *
 * @param <K> 键类型
 * @param <V> 值类型
 * @author Looly
 */
public class FIFOCache<K, V> extends AbstractCache<K, V> {
	private static final long serialVersionUID = 1L;

	/**
	 * 构造,默认对象不过期
	 *
	 * @param capacity 容量
	 */
	public FIFOCache(int capacity) {
		this(capacity, 0);
	}

	/**
	 * 构造
	 *
	 * @param capacity 容量
	 * @param timeout  过期时长
	 */
	public FIFOCache(int capacity, long timeout) {
		this.capacity = capacity;
		this.timeout = timeout;
		cacheMap = new LinkedHashMap<>(Math.max(1 << 4, capacity >>> 7), 1.0f, false);
	}

	/**
	 * 先进先出的清理策略<br>
	 * 先遍历缓存清理过期的缓存对象,如果清理后还是满的,则删除第一个缓存对象
	 */
	@Override
	protected int pruneCache() {
		int count = 0;
		CacheObj<K, V> first = null;

		// 清理过期对象并找出链表头部元素(先入元素)
		Iterator<CacheObj<K, V>> values = cacheMap.values().iterator();
		while (values.hasNext()) {
			CacheObj<K, V> co = values.next();
			if (co.isExpired()) {
				values.remove();
				count++;
			}
			if (first == null) {
				first = co;
			}
		}

		// 清理结束后依旧是满的,则删除第一个被缓存的对象
		if (isFull() && null != first) {
			cacheMap.remove(first.key);
			onRemove(first.key, first.obj);
			count++;
		}
		return count;
	}
}

LFUCache最少使用率缓存

代码如下:

/**
 * LFU(least frequently used) 最少使用率缓存<br>
 * 根据使用次数来判定对象是否被持续缓存<br>
 * 使用率是通过访问次数计算的。<br>
 * 当缓存满时清理过期对象。<br>
 * 清理后依旧满的情况下清除最少访问(访问计数最小)的对象并将其他对象的访问数减去这个最小访问数,以便新对象进入后可以公平计数。
 * 
 * @author Looly,jodd
 *
 * @param <K> 键类型
 * @param <V> 值类型
 */
public class LFUCache<K, V> extends AbstractCache<K, V> {
	private static final long serialVersionUID = 1L;

	/**
	 * 构造
	 * 
	 * @param capacity 容量
	 */
	public LFUCache(int capacity) {
		this(capacity, 0);
	}

	/**
	 * 构造
	 * 
	 * @param capacity 容量
	 * @param timeout 过期时长
	 */
	public LFUCache(int capacity, long timeout) {
		if(Integer.MAX_VALUE == capacity) {
			capacity -= 1;
		}
		
		this.capacity = capacity;
		this.timeout = timeout;
		cacheMap = new HashMap<>(capacity + 1, 1.0f);
	}

	// ---------------------------------------------------------------- prune

	/**
	 * 清理过期对象。<br>
	 * 清理后依旧满的情况下清除最少访问(访问计数最小)的对象并将其他对象的访问数减去这个最小访问数,以便新对象进入后可以公平计数。
	 * 
	 * @return 清理个数
	 */
	@Override
	protected int pruneCache() {
		int count = 0;
		CacheObj<K, V> comin = null;

		// 清理过期对象并找出访问最少的对象
		Iterator<CacheObj<K, V>> values = cacheMap.values().iterator();
		CacheObj<K, V> co;
		while (values.hasNext()) {
			co = values.next();
			if (co.isExpired() == true) {
				values.remove();
				onRemove(co.key, co.obj);
				count++;
				continue;
			}

			//找出访问最少的对象
			if (comin == null || co.accessCount < comin.accessCount) {
				comin = co;
			}
		}

		// 减少所有对象访问量,并清除减少后为0的访问对象
		if (isFull() && comin != null) {
			long minAccessCount = comin.accessCount;

			values = cacheMap.values().iterator();
			CacheObj<K, V> co1;
			while (values.hasNext()) {
				co1 = values.next();
				co1.accessCount -= minAccessCount;
				if (co1.accessCount <= 0) {
					values.remove();
					onRemove(co1.key, co1.obj);
					count++;
				}
			}
		}
		
		return count;
	}
}

LRUCache最近最久未使用缓存

主要是使用LinkedHashMap的LRU自带功能,代码如下:

/**
 * LRU (least recently used)最近最久未使用缓存<br>
 * 根据使用时间来判定对象是否被持续缓存<br>
 * 当对象被访问时放入缓存,当缓存满了,最久未被使用的对象将被移除。<br>
 * 此缓存基于LinkedHashMap,因此当被缓存的对象每被访问一次,这个对象的key就到链表头部。<br>
 * 这个算法简单并且非常快,他比FIFO有一个显著优势是经常使用的对象不太可能被移除缓存。<br>
 * 缺点是当缓存满时,不能被很快的访问。
 * @author Looly,jodd
 *
 * @param <K> 键类型
 * @param <V> 值类型
 */
public class LRUCache<K, V> extends AbstractCache<K, V> {
	private static final long serialVersionUID = 1L;

	/**
	 * 构造<br>
	 * 默认无超时
	 * @param capacity 容量
	 */
	public LRUCache(int capacity) {
		this(capacity, 0);
	}

	/**
	 * 构造
	 * @param capacity 容量
	 * @param timeout 默认超时时间,单位:毫秒
	 */
	public LRUCache(int capacity, long timeout) {
		if(Integer.MAX_VALUE == capacity) {
			capacity -= 1;
		}

		this.capacity = capacity;
		this.timeout = timeout;
		
		//链表key按照访问顺序排序,调用get方法后,会将这次访问的元素移至头部
		cacheMap = new FixedLinkedHashMap<>(capacity);
	}

	// ---------------------------------------------------------------- prune

	/**
	 * 只清理超时对象,LRU的实现会交给<code>LinkedHashMap</code>
	 */
	@Override
	protected int pruneCache() {
		if (isPruneExpiredActive() == false) {
			return 0;
		}
		int count = 0;
		Iterator<CacheObj<K, V>> values = cacheMap.values().iterator();
		CacheObj<K, V> co;
		while (values.hasNext()) {
			co = values.next();
			if (co.isExpired()) {
				values.remove();
				onRemove(co.key, co.obj);
				count++;
			}
		}
		return count;
	}
}

TimedCache定时清理缓存

该缓存提供了定时器来实现定时清理过期的缓存对象。代码如下:

/**
 * 定时缓存<br>
 * 此缓存没有容量限制,对象只有在过期后才会被移除
 * 
 * @author Looly
 *
 * @param <K> 键类型
 * @param <V> 值类型
 */
public class TimedCache<K, V> extends AbstractCache<K, V> {
	private static final long serialVersionUID = 1L;

	/** 正在执行的定时任务 */
	private ScheduledFuture<?> pruneJobFuture;

	/**
	 * 构造
	 * 
	 * @param timeout 超时(过期)时长,单位毫秒
	 */
	public TimedCache(long timeout) {
		this(timeout, new HashMap<>());
	}

	/**
	 * 构造
	 * 
	 * @param timeout 过期时长
	 * @param map 存储缓存对象的map
	 */
	public TimedCache(long timeout, Map<K, CacheObj<K, V>> map) {
		this.capacity = 0;
		this.timeout = timeout;
		this.cacheMap = map;
	}

	// ---------------------------------------------------------------- prune
	/**
	 * 清理过期对象
	 * 
	 * @return 清理数
	 */
	@Override
	protected int pruneCache() {
		int count = 0;
		Iterator<CacheObj<K, V>> values = cacheMap.values().iterator();
		CacheObj<K, V> co;
		while (values.hasNext()) {
			co = values.next();
			if (co.isExpired()) {
				values.remove();
				onRemove(co.key, co.obj);
				count++;
			}
		}
		return count;
	}

	// ---------------------------------------------------------------- auto prune
	/**
	 * 定时清理
	 * 
	 * @param delay 间隔时长,单位毫秒
	 */
	public void schedulePrune(long delay) {
		this.pruneJobFuture = GlobalPruneTimer.INSTANCE.schedule(this::prune, delay);
	}

	/**
	 * 取消定时清理
	 */
	public void cancelPruneSchedule() {
		if (null != pruneJobFuture) {
			pruneJobFuture.cancel(true);
		}
	}

}

WeakCache弱引用缓存

代码如下:

/**
 * 弱引用缓存<br>
 * 对于一个给定的键,其映射的存在并不阻止垃圾回收器对该键的丢弃,这就使该键成为可终止的,被终止,然后被回收。<br>
 * 丢弃某个键时,其条目从映射中有效地移除。<br>
 * 
 * @author Looly
 *
 * @param <K> 键
 * @param <V> 值
 * @author looly
 * @since 3.0.7
 */
public class WeakCache<K, V> extends TimedCache<K, V>{
	private static final long serialVersionUID = 1L;

	public WeakCache(long timeout) {
		super(timeout, new WeakHashMap<K, CacheObj<K, V>>());
	}

}