Redis 的过期策略
一、假设设置一个key 只能存活1h,那么1h之后,Redis 是怎么对这批 key 进行删除的?
回答: 定期删除 + 惰性删除
- 所谓定期删除,指的是 Redis 默认每隔100ms 就随机抽取一些设置了过期时间的 key,检查其是否过期,如果过期就删除。注意,这里可不是每隔 100ms 就遍历所有设置过期时间的key ,那样就是性能的灾难。实际上,Redis 是每隔 100ms 随机抽取一些 key 来检查和删除的。
- 但问题是,定期删除可能会导致过多的 key 到了时间并没有删除掉。所以就需要惰性删除。就是说,在获取某个 key 的时候, Redis 会检查一下,这个 key 如果设置了过期时间,并且是否过期了,如果过期了,此时就会删除。
- 通过上述两种手段结合起来,保证过期的 key 一定回被干掉
- 但是,实际上这样还是存在一些问题的,如果定期删除漏掉了很多过期的 key, 然后你也没有及时去查,也就没走惰性删除,此时会怎么样?如果大量过期的 key 堆积在内存里,导致 Redis 内存耗尽了,那么怎么办呢?
- 答案是:内存淘汰机制
二、内存淘汰机制
如果 Redis 的内存占用过多,此时就会进行内存淘汰,有如下一些策略:
1)noeviction: 当内存不足以容纳新写入的数据时,新写入操作会报错,这个一般没人用吧,实在太恶心了
2)allkeys-lru: 当内存不足以容纳新写入的数据时,在键空间中,移除最近最少使用的key(这个是最常用的)
3)allkeys-random: 当内存不足以容纳新写入的数据时,在键空间中,随机移除某个 key(一般没人用)
4) volatile-lru: 当内存不足以容纳新写入的数据时,在设置了过期时间的键空间中,移除最近最少使用的 key(这个一般不太合适)
5)volatile-random: 当内存不足以容纳新写入的数据时,在设置了过期时间的键空间中,随机移除某个 key
6)volatile-ttl: 当内存不足以容纳新写入的数据时,在设置了过期时间的键空间中,有更早过期时间的 key 先移除
手写一个 LRU 算法
下面是一个简单的手写LRU(Least Recently Used)算法的示例代码,使用双向链表和哈希表实现:
import java.util.HashMap;
class LRUCache {
// 定义双向链表节点
class Node {
int key;
int value;
Node prev;
Node next;
public Node(int key, int value) {
this.key = key;
this.value = value;
}
}
private HashMap<Integer, Node> map; // 哈希表用于快速查找缓存数据
private Node head; // 虚拟头节点
private Node tail; // 虚拟尾节点
private int capacity; // 缓存容量
public LRUCache(int capacity) {
this.capacity = capacity;
map = new HashMap<>();
head = new Node(-1, -1);
tail = new Node(-1, -1);
head.next = tail;
tail.prev = head;
}
// 获取缓存数据
public int get(int key) {
if (map.containsKey(key)) {
Node node = map.get(key);
remove(node); // 从链表中移除当前节点
addFirst(node); // 将当前节点移动到链表头部
return node.value;
}
return -1;
}
// 写入缓存数据
public void put(int key, int value) {
if (map.containsKey(key)) {
Node node = map.get(key);
node.value = value;
remove(node); // 从链表中移除当前节点
addFirst(node); // 将当前节点移动到链表头部
} else {
if (map.size() == capacity) {
map.remove(tail.prev.key); // 移除链表尾部节点对应的缓存数据
remove(tail.prev); // 移除链表尾部节点
}
Node newNode = new Node(key, value);
map.put(key, newNode);
addFirst(newNode); // 将新节点添加到链表头部
}
}
// 从链表中移除节点
private void remove(Node node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
// 将节点添加到链表头部
private void addFirst(Node node) {
Node next = head.next;
head.next = node;
node.prev = head;
node.next = next;
next.prev = node;
}
}
在这个示例中,LRUCache类实现了一个LRU缓存,使用双向链表来维护缓存数据的访问顺序,使用哈希表来快速查找缓存数据。通过get和put方法实现对缓存的读取和写入操作,保持缓存中数据的最近访问顺序。