过期策略
- 定期删除
redis 会将每个设置了过期时间的 key 放入到一个独立的字典中,以后会定期遍历这个字典来删除到期的 key。
Redis 默认会每秒进行十次过期扫描(100ms一次),过期扫描不会遍历过期字典中所有的 key,而是采用了一种简单的贪心策略。
1.从过期字典中随机 20 个 key;
2.删除这 20 个 key 中已经过期的 key;
redis默认是每隔 100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。注意这里是随机抽取的。为什么要随机呢?你想一想假如 redis 存了几十万个 key ,每隔100ms就遍历所有的设置过期时间的 key 的话,就会给 CPU 带来很大的负载。
- 惰性删除
所谓惰性策略就是在客户端访问这个key的时候,redis对key的过期时间进行检查,如果过期了就立即删除,不会给你返回任何东西。
定期删除可能会导致很多过期key到了时间并没有被删除掉。所以就有了惰性删除。假如你的过期 key,靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一下那个 key,才会被redis给删除掉。这就是所谓的惰性删除,即当你主动去查过期的key时,如果发现key过期了,就立即进行删除,不返回任何东西.
redis中的过期测策越
是定期删除和懒性删除结合使用的,
为什么需要淘汰策略
有了以上过期策略的说明后,就很容易理解为什么需要淘汰策略了,因为不管是定期采样删除还是惰性删除都不是一种完全精准的删除,就还是会存在key没有被删除掉的场景,所以就需要内存淘汰策略进行补充。
内存淘汰策略
1. noeviction:当内存使用超过配置的时候会返回错误,不会驱逐任何键
2. allkeys-lru:加入键的时候,如果过限,首先通过LRU算法驱逐最久没有使用的键(hash链表的尾部移除一个元素)
3. volatile-lru:加入键的时候如果过限,首先从设置了过期时间的键集合中驱逐最久没有使用的键(hash链表的尾部移除一个元素)
4. allkeys-random:加入键的时候如果过限,从所有key随机删除
5. volatile-random:加入键的时候如果过限,从过期键的集合中随机驱逐
6. volatile-ttl:从配置了过期时间的键中驱逐马上就要过期的键
7. volatile-lfu:从所有配置了过期时间的键中驱逐使用频率最少的键
8. allkeys-lfu:从所有键中驱逐使用频率最少的键
涉及LRU和LFU算法
LRU算法(allkeys-lr, volatile-lru)
LRU算法使用了一种有趣的数据结构,叫做【哈希链表】
依靠哈希链表的【有序性】,可以把Key-Value按照操作来【排序】
LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
- 新数据插入到链表头部;
- 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
- 当链表满的时候,将链表尾部的数据丢弃。
过程如下:
手撕LRU算法
LRU算法的缺陷:一个很久没有被访问的key,偶尔被访问一次,导致被误认为是热点数据的问题(该数据会被移至表头)
基本思路
简单的LRU算法,手写LRU算法
public class LRUCache {
private Node head;
private Node tail;
private final HashMap<String,Node> nodeHashMap;
private int capacity;
public LRUCache(int capacity){
this.capacity=capacity;
nodeHashMap=new HashMap<>();
head=new Node();
tail=new Node();
head.next=tail;
tail.prev=head;
}
private void removeNode(Node node){
if(node==tail){
tail=tail.prev;
tail.next=null;
}else if(node==head){
head=head.next;
head.prev=null;
}else {
node.prev.next=node.next;
node.next.prev=node.prev;
}
}
private void addNodeToHead(Node node){
node.next=head.next;
head.next.prev=node;
node.prev=head;
head.next=node;
}
private void addNodeToTail(Node node){
node.prev=tail.prev;
node.prev.next=node;
node.next=tail;
tail.prev=node;
}
//当链表中的某个缓存被命中时,直接把数据移到链表头部,原本在头节点的缓存就向链表尾部移动
public void moveNodeToHead(Node node){
removeNode(node);
addNodeToHead(node);
}
public String get(String key){
Node node=nodeHashMap.get(key);
if(node==null){
return null;
}
//刷新当前节点的位置
moveNodeToHead(node);
//返回value值
return node.value;
}
public void put(String key,String value){
Node node=nodeHashMap.get(key);
if(node==null){ //不存在
//如果当前存储的数据量达到了阈值,则需要淘汰掉访问较少的数据
if(nodeHashMap.size()>=capacity){
removeNode(tail); //移除尾部节点
nodeHashMap.remove(tail.key);
}
node=new Node(key,value);
nodeHashMap.put(key,node);
addNodeToHead(node);
}else{
node.value=value;
//刷新当前节点的位置
moveNodeToHead(node);
}
}
public static void main(String[] args) {
LRUCache lruCache=new LRUCache(3);
lruCache.put("1","1");
lruCache.put("2","2");
lruCache.put("3","3");// lruCache.get("3");
// 增加一个访问次数之后,被清理的元素就会发生变化
System.out.println(lruCache.nodeHashMap);
lruCache.put("4","4");
System.out.println(lruCache.nodeHashMap);
}
}
class Node{
//双向链表中的节点类,存储key是因为我们在双向链表删除表尾的值时,只是返回了一个节点,
//所以这个节点要包括key值,这样我们的哈希表才可以删除对应key值的映射
public String key;
public String value;
Node prev;
Node next;
public Node(){}
public Node(String key, String value) {
this.key = key;
this.value = value;
}
}
LFU算法
LFU(Least Frequently Used),key的使用次数有关,其思想是:根据key最少被访问的次数进行淘汰,比较少访问的key优先淘汰,反之则保留。
LFU的原理是使用计数器来对key进行排序,每次key被访问时,计数器会增大,当计数器越大,意味着当前key的访问越频繁,也就是意味着它是热点数据。
LFU维护了两个链表,横向组成的链表用来存储访问次数,每个访问次数的节点下存储一个双向链表,这个双向链表存储的是具有相同访问次数的缓存数据。
具体的工作原理是:
•当添加元素时,找到相同访问次数的节点,然后添加到该节点的数据链表的头部。如果该数据链表满了,则移除链表尾部的节点
•当获取元素或者修改元素时,都会增加对应key的访问次数,并把当前节点移动到对应的访问次数节点下。
添加元素时,访问次数默认为1,随着访问次数的增加,访问次数也不断递增。而当前被访问的元素随着访问次数增加进行移动到不同访问次数的节点下