1.概念
本地缓存,在我们日常开发中是必不可少的一种解决数据读取性能问题的方法。简单的说,cache 就是为了提升系统性能而开辟的一块内存空间,SpringBoot 1.x版本中的默认本地cache是Guava Cache。在2.x(Spring Boot 2.0(spring 5) )版本中已经用Caffine Cache取代了Guava Cache。毕竟有了更优的缓存淘汰策略。这2个都是线程安全的,可以指定容量,多种过期策略,主要特性是将数据写入缓存时是原子操作。当缓存的数据达到最大规模时,会使用“最近最少使用(LRU)”算法来清除缓存数据。
每一条数据还可以基于时间回收,未使用时间超过一定时间后,数据会被回收。当缓存被清除时,会发送通知告知。还提供访问统计功能。
2.缓存常见淘汰算法
淘汰算法其实就是当缓存被用满时清理数据的优先顺序
2.1 FIFO(First In First out)
算法原理按照“先进先出(First In,First Out)”的原理淘汰数据。
实现步骤原理如下:
1. 新访问的数据插入FIFO队列尾部,数据在FIFO队列中顺序移动;
2. 淘汰FIFO队列头部的数据;
局限性:在这种淘汰算法中,先进入缓存的会先被淘汰,会导致命中率很低。
2.2 LRU(Least recently used)Guava Cache利用的就是此算法
算法的原理根据数据的历史访问记录来进行数据淘汰。核心思想“如果数据最近被访问过,那么将来被访问的几率也更高”。实现步骤原理如下:
1. 新数据插入到链表头部;
2. 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
3. 当链表满的时候,将链表尾部的数据丢弃。
优点和局限性:如果有个数据在 1 分钟访问了 1000次,再后 1 分钟没有访问这个数据,但是有其他的数据访问,就导致了我们这个热点数据被淘汰。LRU可以很好的应对突发流量的情况,因为他不需要累计数据频率。但LRU通过历史数据来预测未来是局限的,它会认为最后到来的数据是最可能被再次访问的,从而给与它最高的优先级。
2.3 LFU(Least frequently used)
算法的原理根据数据的历史访问频率来进行数据淘汰。其核心思想“如果数据过去被访问多次,那么将来被访问的频率也更高”。LFU的每个数据块都有一个引用计数,所有数据块按照引用计数排序,具有相同引用计数的数据块则按照时间排序。
,通过利用额外的空间记录每个数据的使用频率,然后选出频率最低进行淘汰。这样就避免了 LRU 不能处理时间段的问题,其具体实现步骤原理如下:
1. 新加入数据插入到队列尾部(因为引用计数为1);
2. 队列中的数据被访问后,引用计数增加,队列重新排序;
3. 当需要淘汰数据时,将已经排序的列表最后的数据块删除。
优点:在 LFU 中只要数据访问模式的概率分布随时间保持不变时,其命中率就能变得非常高。
局限性:1需要给每个记录项维护频率信息,每次访问都需要更新,这是个巨大的开销 2如果数据访问模式随时间有变,LFU的频率信息无法随之变化,因此早先频繁访问的记录可能会占据缓存,而后期访问较多的记录则无法被命中
2.4 TinyLFU Caffine Cache利用的就是此算法的变种算法
背景: 前三种策略各有利弊,实现的成本也是一个比一个高,同时命中率也是一个比一个好。Guava Cache虽然有这么多的功能,但是本质上还是对LRU的封装,大多数的缓存设计都是基于LRU或者其变种来进行的。相比之下,LRU并不需要维护昂贵的缓存记录元信 息,同时也能够反应随时间变化的数据访问模式。然而,在许多负载之下,LRU依然需要更多的空间才能做到跟LFU一致的缓存命中率。而TinyLFU缓存有综合两者的长处。
1维护记录项频率信息:TinyLFU借助了数据流Sketching技术,Count-Min Sketch(布隆过滤器的一种变种)可以用小得多的空间存放频率信息
2反应随时间变化的访问模式:TinyLFU采用了一种基于滑动窗口的时间衰减设计机制,借助于一种简易的reset操作:每次添加一条记录到Sketch的时候,都会给一个计数器上加1,当计数器达到一个尺寸W的时候,把所有记录的Sketch数值都除以2,该reset操作可以起到衰减的作用
2.5 JetCache 综合框架
背景: JetCache 是一个基于 Java 的缓存系统封装,提供统一的 API 和注解来简化缓存的使用。JetCache 提供了比 SpringCache 更加强大的注解,可以原生的支持 TTL、两级缓存、分布式自动刷新,还提供了 Cache 接口用于手工缓存操作。当前有四个实现,RedisCache、TairCache(此部分未在 github 开源)、CaffeineCache(in memory)和一个简易的 LinkedHashMapCache(in memory),要添加新的实现也是非常简单的。
具体示例参考官网文档
3.使用
3.1 缓存填充策略:一般是手动加载,同步加载,异步加载
3.2 回收策略:
基于大小回收:maximumWeight,maximumSize 两者不可以同时使用。
基于时间回收,expireAfterAccess expireAfterWrite
基于引用回收:weakKeys():使用弱引用存储键。weakValues():使用弱引用存储值。softValues():使用软引用存储值。
4代码示例
4.1基于LRU算法 ,LinkedHashMap已经很好的实现了,这里在造个轮子
public class LRUCache {
/**
* 头结点
*/
private Node head;
/**
* 结束结点
*/
private Node end;
/**
* 缓存存储上线
*/
private int limit;
private HashMap<String,Node> hashMap;
public LRUCache(int limit){
this.limit=limit;
hashMap=new HashMap<String,Node>();
}
public String get(String key){
Node node=hashMap.get(key);
if(node ==null){
return null;
}
refreshNode(node);
return node.value;
}
public void put(String key,String value){
Node node=hashMap.get(key);
if(node ==null){
//如果key不存在,则插入key-value
if(hashMap.size()>=limit){
String oldKey=removeNode(head);
hashMap.remove(oldKey);
}
node =new Node(key,value);
addNode(node);
hashMap.put(key,node);
}else{
//如果key存在。则刷新key-value
node.value=value;
refreshNode(node);
}
}
/**
* 删除节点
* @param key
*/
public void remove(String key){
Node node =hashMap.get(key);
removeNode(node);
hashMap.remove(node);
}
/**
* 尾部插入结点
* @param node
*/
private void addNode(Node node){
if(end !=null){
end.next=node;
node.pre=end;
node.next=null;
}
end=node;
if(head ==null){
head=node;
}
}
/**
* 要删除的节点
* @param node
* @return
*/
private String removeNode(Node node){
if(node == head && node ==end){
//移除唯一的节点
head =null;
end=null;
}else if(node ==end){
//移除尾节点
end=end.pre;
end.next=null;
}else if(node ==head){
head=head.next;
head.pre=null;
}else{
node.pre.next=node.next;
node.next.pre=node.pre;
}
return node.key;
}
/**
* 刷新被访问的节点位置
* @param node
*/
private void refreshNode(Node node){
//如果访问的节点是尾节点,则无须移动节点
if(node ==end){
return;
}
//移除节点
removeNode(node);
//重新插入节点
addNode(node);
}
class Node{
public Node pre;
public Node next;
public String key;
public String value;
Node(String key,String value){
this.key=key;
this.value=value;
}
}
public static void main(String[] args) {
LRUCache lruCache=new LRUCache(5);
lruCache.put("001","用户1的信息");
lruCache.put("002","用户1的信息");
lruCache.put("003","用户1的信息");
lruCache.put("004","用户1的信息");
lruCache.put("005","用户1的信息");
lruCache.get("002");
lruCache.put("004","用户2的信息更新");
lruCache.put("006","用户6的信息");
System.out.println(lruCache.get("001"));
System.out.println(lruCache.get("006"));
}
5参考链接
Guava:
https://blog.csdn.net/jiangzhexi/article/details/55209887
https://github.com/CoderLyd/myblog/issues/5#issue-577736339
Caffine Cache:
https://www.cnblogs.com/rickiyang/p/11074158.html