1.redis中的内存占满后,当一个新的插入数据到来的时候.redis会怎么做?
如图
1.如果redis中存有静态数据,且不超时,不希望被删除,那么可以使用第二类
重点说下.lru策略和lfu策略
lru策略
常见的lru是通过一个 hashmap和链表组成. map用于存储数据,链表用于存储数据的使用情况
贴一个我的实现
package 链表;
import java.util.HashMap;
import java.util.Map;
/**
* 146题
* 运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制 。
* 实现 LRUCache 类:
* <p>
* LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存
* int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
* void put(int key, int value) 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。
*
* <p>
* 输入
* ["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
* [[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
* 输出
* [null, null, null, 1, null, -1, null, -1, 3, 4]
*
*
* @version 1.0.0, 2020/12/3-2:22 PM.
* @since 2020/12/3-2:22 PM
*/
class LRUCache {
private Map<Integer, Node> data = new HashMap();
private Node head = new Node();
private Node tail = new Node();
private int capacity;
public LRUCache(int capacity) {
this.capacity = capacity;
head.setNext(tail);
tail.setPre(head);
}
public int get(int key) {
Node v = data.get(key);
if (v == null) {
return -1;
} else {
//注意这里也需要移动node到头
v.getNext().setPre(v.getPre());
v.getPre().setNext(v.getNext());
//2.把当前节点移动到头节点
v.setNext(head.getNext());
v.setPre(head);
v.getNext().setPre(v);
head.setNext(v);
return v.getValue();
}
}
public static void main(String[] args) {
LRUCache lruCache = new LRUCache(1);
lruCache.put(2, 1);
System.out.println(lruCache.get(2));
lruCache.put(3, 2);
System.out.println(lruCache.get(2));
}
public void put(int key, int value) {
Node v = data.get(key);
//说明是新增的1.判断长度是否超长,如果超长,那么淘汰尾部数据
if (v == null) {
//数据满了 删除最后一个节点
if (data.size() == capacity) {
//-1
Node last = tail.getPre();
//-2
Node lastPre = last.getPre();
lastPre.setNext(tail);
tail.setPre(lastPre);
data.remove(last.getKey());
}
//不管满不满都要进行的操作1.插入数据 2.将数据移动到head
Node currentNode = new Node(key, value);
data.put(key, currentNode);
//当前的上一个设置为头节点
currentNode.setPre(head);
//设置当前的下一个节点为oldFirst,oldFirst的下一个节点是当前节点
Node first = head.getNext();
currentNode.setNext(first);
first.setPre(currentNode);
head.setNext(currentNode);
} else {
//说明是已有数据,不会涉及到数据淘汰,只涉及到节点移动.
//1.把当前节点前后节点处理了
v.getNext().setPre(v.getPre());
v.getPre().setNext(v.getNext());
//2.把当前节点移动到头节点
v.setNext(head.getNext());
v.setPre(head);
v.getNext().setPre(v);
head.setNext(v);
v.setValue(value);
}
}
}
class Node {
private Node next;
private Node pre;
private int value;
private int key;
public Node(int key, int value) {
this.value = value;
this.key = key;
}
public Node() {
super();
}
public int getKey() {
return key;
}
public void setKey(int key) {
this.key = key;
}
public Node getPre() {
return pre;
}
public void setPre(Node pre) {
this.pre = pre;
}
public Node getNext() {
return next;
}
public void setNext(Node next) {
this.next = next;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
}
/**
* Your LRUCache object will be instantiated and called as such:
* LRUCache obj = new LRUCache(capacity);
* int param_1 = obj.get(key);
* obj.put(key,value);
*/
可以看出,如果想实现lru,需要有一个链表,head代表最近访问的,tail代表很久之前访问的.
如果redis中每个key都存放在这个链表中,那么内存需要很大!!!
因此redis实现了一个简化版的lru.主要目的是减少 内存占用!.
1.随机选取n个key,组成一个资源池.根据每个key上存储的时间戳(最近访问).可以知道谁是最近访问的key(时间戳最大的),可以进行排序.淘汰的时候,选择时间戳最小的.
2.第二次,会再选取m个key放到资源池,但是存放时,有一些规定.就是时间戳要比池子中最小的还要小.是为了不用每次都去排序吧?.当内存不足时,就可以淘汰最小的key了.
此种方式,不用使用链表,局部化的lru
可以看下redis中的官网
The reason why Redis does not use a true LRU implementation is because it costs more memory. However the approximation is virtually equivalent for the application using Redis. The following is a graphical comparison of how the LRU approximation used by Redis compares with true LRU.
lfu策略
lfu在lru的基础上,删除数据的时候,不是直接看谁最近没有被使用,而是先看谁被调用的少.因此需要增加一个计数器.当需要删除数据的时候, 1.判断计数 2.计数相同时,判断谁调用时间戳靠前
如图.RedisObject有一个24位的字段,前16位存放最近访问的时间戳,后8位存放访问次数.
问题1:
1.8位,那么最大值就是2^9-1=255.如果每次访问都加1,那岂不是很快就满了,那请求256次和请求256000次没有区别了.
解决:在实现 LFU 策略时,Redis 并没有采用数据每被访问一次,就给对应的 counter 值加 1 的计数规则,而是采用了一个更优化的计数规则。
下面这段 Redis 的部分源码,显示了 LFU 策略增加计数器值的计算逻辑。其中,baseval 是计数器当前的值。计数器的初始值默认是 5(由代码中的 LFU_INIT_VAL 常量设置),而不是 0,这样可以避免数据刚被写入缓存,就因为访问次数少而被立即淘汰。
double r = (double)rand()/RAND_MAX;
...
double p = 1.0/(baseval*server.lfu_log_factor+1);
if (r < p) counter++;
使用了这种计算规则后,我们可以通过设置不同的 lfu_log_factor 配置项,来控制计数器值增加的速度,避免 counter 值很快就到 255 了。
可以看到 参数为100后,效果很明显
2.如果一个数据第一分钟,频繁的访问. 后面1小时都不访问了,咋整
Redis 在实现 LFU 策略时,还设计了一个 counter 值的衰减机制。
lfu_decay_time 参数,衰减次数= (当前时间-最近访问时间)/lfu_decay_time
参考文章:https://time.geekbang.org/column/article/297270