缓存失效策略
一、 为什么要考虑缓存失效
在一个简单的调用二元关系调用链中请求中调用响应者,响应者往往在性能受限的情况下,在自身已经做完性能优化的基础上,仍有加快响应性能的诉求条件下,在二者之间加入缓存是个常见选择方案,缓存相对原先的响应者在性能更具优势的条件下,往往空间相对响应者本身是较小且有限的,那么在缓存空间已被使用完,新的数据需要加入缓存时,就需要考虑已有的旧数据如何清除掉以便腾出空间给新数据使用。
使用缓存这种方式的有效性基于两个局部性原理:
时间局部性: 最近被使用的数据在近期有大概率还会被用到 ;
空间局部性: 最近被使用的数据的紧密关联数据在近期有大概率被用到 。
二、 缓存失效的常见方式
在考虑采用什么方式进行管理缓存失效时,我们需要牢记我们用缓存的目的是减轻原有响应者的压力,那么数据尽可能多的在缓存中被命中,也就相对说明缓存的利用率越高,(命中缓存的请求数/总的请求数)的比率也就是缓存命中率的高低是评估方案优劣的一个合理标准。
从实践的情况看缓存失效的方式主要有 :
1、随机方式 :用随机函数选择已有的旧数据,实现方式简单粗暴,概率上平均,但实际缓存的数据被使用的概率是不一样的,有可能将热数据删除,造成缓存命中率降低,效果不稳定
2、先进先出方式:从时间维度上线性的考虑数据被加载进缓存的顺序,利用了时间局部性,但结合数据被使用的不均衡性,未必先加载进去的利用率就低,所以效果也不是很理想,不过实现上较为简单,维护个队列即可。
3、最近最少使用方式:从数据被使用的最近一次时间上进行缓存数据的排序,将最近最少使用(也就是最长时间没被使用)的数据清除掉,实现上只要动态维护数据标识的最近使用时间点的线性排序,相对也简单。
4、最不经常使用方式:由于数据在被使用的频率上存在差别,也就是说有冷热数据的区分,所以从使用次数上进行比较,是一种提升命中率的方式,不过实现上由于要统计次数信息且要选择时间窗口长度会增加复杂度。
三、 失效方式的简略实现
1、缓存接口定义
/**
* 模拟缓存
* 用int类型的数据,
* key代表数据标识,
* value代表数据内容,
* key和value的值可以一样,不影响模拟
*
* 缓存设置 capacity 表示容量大小
*/
public interface Cache {
/**
* get 方法代表读缓存
* @param key
* @return
*/
int get(int key) ;
/**
* put方法代表写缓存
* @param key
* @param value
*/
void put(int key, int value) ;
}
2、Random 方式
/**
* @author : wangchaodee
* @Description: 随机方式
*/
public class RandomCache implements Cache {
int[] cache ;
Map<Integer,Integer> keyToIndex;
int[] indexToKey;
private Random random ;
int capacity ;
public RandomCache(int capacity) {
// 控制容量大小
this.capacity = capacity ;
cache = new int[capacity];
keyToIndex = new HashMap<>();
indexToKey = new int[capacity];
// 用random 来模拟随机方式
random = new Random();
}
@Override
public int get(int key) {
if(!keyToIndex.containsKey(key)) return -1 ;
// 先取得位置指针 ,再拿数据
int index = keyToIndex.get(key);
return cache[index];
}
@Override
public void put(int key, int value) {
if(keyToIndex.containsKey(key)){
// 更新对应缓存 ,位置信息不用改变
int idx = keyToIndex.get(key);
cache[idx] = value;
}else {
int size = keyToIndex.size();
if (size < capacity) {
// 用map的size做位置指针 递增添加数据
cache[size] = value;
keyToIndex.put(key, size);
indexToKey[size]=key;
} else {
// 随机取得替换位置 先删除索引中的项 ,再覆盖写入并替换
int idx = random.nextInt(size);
keyToIndex.remove(indexToKey[idx]);
cache[idx] = value;
keyToIndex.put(key, idx);
indexToKey[idx]=key;
}
}
}
}
3、 FIFO 方式
/**
* @author : wangchaodee
* @Description: 先进先出方式
*/
public class FIFOCache implements Cache {
int[] cache ;
Map<Integer,Integer> keyIndexMap ;
int[] indexToKey;
int idx ;
int capacity ;
public FIFOCache(int capacity) {
// 控制容量大小
cache = new int[capacity];
keyIndexMap = new HashMap<>();
indexToKey = new int[capacity];
this.idx =0 ;
this.capacity = capacity ;
}
@Override
public int get(int key) {
if(!keyIndexMap.containsKey(key)) return -1 ;
// 先取得位置指针 ,再拿数据
int idx = keyIndexMap.get(key);
return cache[idx];
}
@Override
public void put(int key, int value) {
if(keyIndexMap.containsKey(key)){
// 更新对应缓存 ,位置信息不用改变
int idx = keyIndexMap.get(key);
cache[idx] = value;
}else {
int size = keyIndexMap.size();
if (size < capacity) {
// 用map的size做位置指针 递增添加数据
cache[idx] = value;
keyIndexMap.put(key, idx);
indexToKey[idx]=key;
} else {
// 覆盖掉idx指向的当前位置数据
keyIndexMap.remove(indexToKey[idx]);
cache[idx] = value;
keyIndexMap.put(key, idx);
indexToKey[idx]=key;
}
// 指针后移一位 循环
idx = (idx + 1) % capacity;
}
}
}
4、 LRU 方式
/**
* 可参考力扣 146. LRU 缓存
*/
public class LRUCache implements Cache {
private Map<Integer, DLinkNode> cache;
// 维护一个队列 来表示数据近期被使用的顺序
private DLinkList queue;
private int capacity;
public LRUCache(int capacity) {
this.capacity = capacity;
cache = new HashMap<>();
queue = new DLinkList();
}
public int get(int key) {
if (!cache.containsKey(key)) {
return -1;
}
makeRecently(key);
return cache.get(key).value;
}
public void put(int key, int value) {
if(cache.containsKey(key)){
deleteKey(key);
addRecently(key,value);
return;
}
if (capacity == queue.size()) {
//删除最近最少使用
removeLeastRecently();
}
addRecently(key,value);
}
private void makeRecently(int key){
DLinkNode node = cache.get(key);
// 将节点移动到队列尾部代表 最近被使用
queue.removeNode(node);
queue.addToTail(node);
}
private void addRecently(int key ,int value){
DLinkNode node = new DLinkNode(key,value);
queue.addToTail(node);
cache.put(key,node);
}
private void deleteKey(int key){
DLinkNode node = cache.get(key);
queue.removeNode(node);
cache.remove(key);
}
private void removeLeastRecently(){
DLinkNode node = queue.removeFirst();
if(node !=null) cache.remove(node.key);
}
class DLinkNode {
public int key;
public int value;
public DLinkNode left;
public DLinkNode right;
DLinkNode(int key, int value) {
this.key = key;
this.value = value;
}
}
class DLinkList {
private DLinkNode head , tail ;
private int size;
public DLinkList(){
this.head = new DLinkNode(0,0);
this.tail = new DLinkNode(0,0);
head.right = tail;
tail.left = head;
size=0;
}
public void addToTail(DLinkNode node) {
//尾部插入
node.left = tail.left;
node.right = tail;
tail.left.right = node;
tail.left = node;
size++;
}
public DLinkNode removeFirst() {
if(head.right == tail ) return null ;
DLinkNode node = head.right;
removeNode(node);
return node;
}
private void removeNode(DLinkNode node) {
// 去除
node.left.right = node.right;
node.right.left = node.left;
size--;
}
public int size(){
return size ;
}
}
}
5、 LFU 方式
/**
* 可参考力扣 460. LFU 缓存
*/
public class LFUCache implements Cache{
// time 代表时间器
int capacity, time;
Map<Integer, Node> cache;
// 统计数据 以树形排序
TreeSet<Node> statistic;
public LFUCache(int capacity) {
if(capacity == 0 ) throw new IllegalArgumentException();
this.capacity = capacity ;
this.time = 0 ;
cache = new HashMap<>();
statistic = new TreeSet<>() ;
}
public int get(int key) {
if(!cache.containsKey(key))
return -1 ;
Node node = cache.get(key);
statistic.remove(node);
node.cnt +=1;
// 更新时间戳
node.time = ++time ;
statistic.add(node);
cache.put(key,node);
return node.value;
}
public void put(int key, int value) {
if(!cache.containsKey(key)){
if(cache.size()==capacity){
cache.remove(statistic.first().key);
statistic.remove(statistic.first());
}
Node node = new Node(key,value ,1, ++time);
cache.put(key,node);
statistic.add(node);
}else {
Node node = cache.get(key);
statistic.remove(node);
node.cnt +=1;
// 更新时间戳
node.time = ++time ;
node.value = value ;
statistic.add(node);
cache.put(key,node);
}
}
class Node implements Comparable<Node> {
// cnt 代表使用次数, time 最近的一次时间 值越大代表越近期
int key, value, cnt, time;
public Node(int key, int value, int cnt, int time) {
this.key = key;
this.value = value;
this.cnt = cnt;
this.time = time;
}
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (other instanceof Node) {
Node node = (Node) other;
return this.cnt == node.cnt && this.time == node.time;
}
return false;
}
@Override
public int compareTo(Node o) {
return cnt == o.cnt ? time - o.time : cnt - o.cnt;
}
public int hashCode() {
return cnt * 1000000007 + time;
}
}
}
6、命中率模拟测试
/**
* @author : wangchaodee
* @Description: 对cache命中率的模拟测试
*/
public class CacheTest {
@Test
public void testCache(){
int count = 20000 ;
int randomMax = 50 ;
int capacity = 10 ;
int[] array = new int[count];
//int[] array = new int[2*count];
Random random = new Random();
for (int i = 0; i < count; i++) {
array[i] = random.nextInt(randomMax);
}
// 将小区间值增大
// randomMax /=2 ;
// for (int i = 0; i < count; i++) {
// array[i] = random.nextInt();
// }
// 随机化数组 打乱 将前后两部分数据混合均匀
//ShuffleArray shuffleArray = new ShuffleArray(array);
//array = shuffleArray.shuffle();
List<Cache> caches = new ArrayList<>() ;
caches.add(new FIFOCache(capacity));
caches.add(new RandomCache(capacity));
caches.add(new LRUCache(capacity));
caches.add(new LFUCache(capacity));
for (int i = 0; i < caches.size(); i++) {
Cache cache = caches.get(i) ;
int cnt = testCache(array , cache);
//(float)cnt/(2*count)
System.out.printf(" %s 的命中率为 : %.3f , \n" ,cache.getClass().getName() , (float)cnt/(count) );
}
}
/**
*
* @param array 用非负数的数组作为测试集: array[i]的值作为key , array[i]+1 代表value
* @param cache
*/
private int testCache(int[] array , Cache cache){
int cnt =0 ; // 统计缓存命中次数
for (int i = 0; i < array.length; i++) {
if( cache.get(array[i]) == -1 ) {
cache.put(array[i] ,array[i]+1);
}else if(cache.get(array[i]) == array[i]+1 ) {
cnt++;
}else {
// 缓存数据错误 检查代码后测试
System.out.printf("缓存数据错误 检查代码后测试 key: %s ,value: %s \n" ,array[i] ,cache.get(array[i]) );
}
}
return cnt ;
}
}
测试结果示例
数据等可能性下测试,命中率差别不大 ; 提高部分数据概率后,LRU 和LFU 的命中率会更高;不过这只是模拟测试,结果只做参考,提供思路,实际应用还需要用实际的缓存和数据进行测试 。
数据等可能性下测试
int count = 200 ;
int randomMax = 50 ;
int capacity = 10 ;
FIFOCache 的命中率为 : 0.230 ,
RandomCache 的命中率为 : 0.215 ,
LRUCache 的命中率为 : 0.235 ,
LFUCache 的命中率为 : 0.210 ,
capacity/randomMax 的比率为0.2 命中率接近此数值
int count = 20000 ;
int randomMax = 50 ;
int capacity = 20 ;
FIFOCache 的命中率为 : 0.397 ,
RandomCache 的命中率为 : 0.387 ,
LRUCache 的命中率为 : 0.387 ,
LFUCache 的命中率为 : 0.395 ,
capacity/randomMax 的比率为0.4 命中率接近此数值
提高部分数据概率后
int count = 20000 ; // 乘以2
int randomMax = 50 ; // // 将小区间值增大
int capacity = 10 ;
需要混合打乱数组
FIFOCache 的命中率为 : 0.454 ,
RandomCache 的命中率为 : 0.454 ,
LRUCache 的命中率为 : 0.499 ,
LFUCache 的命中率为 : 0.500 ,