前言
在我们的常规开发流程中,利用Redis来优化数据查询是一种常用且在效果上表现出色的策略。我们会精选一些读取频繁且更新频率不高的数据,将这些数据存储到Redis内。这样做的优点在于,每当有数据请求来临,我们可以直接从Redis中以极快的速度取得数据,避免了对数据库进行繁琐的查询。这一方式不仅让查询速度大大提升,同时也有效减轻了数据库的压力。然而,当Redis内存用量超过预设上限时,找到一个合适的方式来处理新进的数据请求就显得至关重要。正因如此,Redis提供了以下几种内存淘汰策略供我们选择:
- noeviction:这是默认的策略,当内存使用超过预设上限时,新进的写入操作将被拒绝,同时会返回一个错误消息。
- allkeys-lru:此策略在将新数据添加到内存时,会按照最近最少使用(Least Recently Used, LRU)的原则,选择最不常用的数据进行淘汰。
- volatile-lru:此策略在一定程度上与allkeys-lru类似,但它仅淘汰那些已设定过期时间的键值。
- allkeys-random:当内存使用超出预设上限时,此策略会随机选择数据进行淘汰。
- volatile-random:此策略类似于allkeys-random,唯一的区别在于,它仅对已设定过期时间的键值进行随机淘汰。
- volatile-ttl:此策略会优先淘汰那些快要过期的键值。在需要释放内存时,会参考键值的过期时间进行淘汰。 以上六种策略,可以根据实际业务需求和特性进行选择,以确保Redis能在内存资源有限的情况下,仍能高效运行。
这篇问题我们简单聊一聊LRU算法
LRU算法实现思路
我们使用1. 双向链表+哈希表:其中,双向链表按照元素被访问的时序顺序进行排序,越近期被访问的元素越靠近表头,越久未被访问的元素越靠近表尾。而哈希表则提供了查找任何元素的 O(1) 复杂度。当需要访问数据时,我们首先在哈希表中进行查找,如果存在则立刻返回,并更新双向链表(将此元素移动到表头),若不存在则需要从数据源获取数据,然后添加至表头,并在哈希表中增加相应的记录。如果此时缓存已满,则需要移除表尾元素,并在哈希表中移除相应记录。
代码实现
js
public class LruCache {
/**
* 内部类,节点对象
*/
class Node {
/**
* key
*/
private int key;
/**
* value
*/
private int value;
/**
* 前置节点
*/
private Node prev;
/**
* 后置节点
*/
private Node next;
Node() {}
Node(int key, int value) {
this.key = key;
this.value = value;
}
public int getKey() {
return key;
}
public void setKey(int key) {
this.key=key;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value=value;
}
public Node getPrev() {
return prev;
}
public void setPrev(Node prev) {
this.prev=prev;
}
public Node getNext() {
return next;
}
public void setNext(Node next) {
this.next=next;
}
}
/**
* 数据队列
*/
private Map<Integer, Node> cache = new HashMap<>();
/**
* 当前队列大小
*/
private int size;
/**
* 队列最大容量
*/
private int capacity;
/**
* 前置节点
*/
private Node head;
/**
* 尾节点
*/
private Node tail;
/**
* 初始化
* @param capacity 容器最大容量
*/
public LruCache(int capacity) {
this.size = 0;
this.capacity = capacity;
// 使用伪头部和伪尾部节点
head = new Node();
tail = new Node();
head.next = tail;
tail.prev = head;
}
/**
* 获取队列中的元素
* @param key key
* @return 节点
*/
public int get(int key) {
Node node = cache.get(key);
if (node == null) {
return -1;
}
// 如果 key 存在,先通过哈希表定位,再移到头部
moveToHead(node);
return node.value;
}
/**
* 将元素存入队列中
* @param key key
* @param value value
*/
public void put(int key, int value) {
Node node = cache.get(key);
if (node == null) {
// 如果 key 不存在,创建一个新的节点
Node newNode = new Node(key, value);
// 添加进哈希表
cache.put(key, newNode);
// 添加至双向链表的头部
addToHead(newNode);
++size;
if (size > capacity) {
// 如果超出容量,删除双向链表的尾部节点
Node tail = removeTail();
// 删除哈希表中对应的项
cache.remove(tail.key);
--size;
}
} else {
// 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
node.value = value;
moveToHead(node);
}
}
/**
* 将当前节点添加至队列头节点
* @param node 节点
*/
private void addToHead(Node node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
/**
* 从队列中删除节点
* @param node 节点
*/
private void removeNode(Node node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
/**
* 从队列获取到node节点后,将node节点移动至队列头节点
* @param node node
*/
private void moveToHead(Node node) {
removeNode(node);
addToHead(node);
}
/**
* 删除尾节点
* @return 尾节点node
*/
private Node removeTail() {
Node res = tail.prev;
removeNode(res);
return res;
}
}
总结
LRU算法是一种常见的缓存淘汰策略,其核心思想是:如果一个数据在最近一段时间没有被访问过,那么在将来它被访问的可能性也很小。因此,当缓存空间需要被新的数据占用时,我们可以淘汰那些最近最少使用的数据。
具体实现上,常用的方法是用一个队列来存储缓存数据,并且保证队列的最大长度等于缓存的最大容量。当新的数据进来时,我们从队尾插入,当数据再次被访问时,我们将它移动到队头。这样我们只需要在缓存满的时候,直接淘汰队尾的数据即可。
LRU算法的优点在于其实现简单,能够维持较好的缓存性能。而且,对于访问模式分布较为均匀,或者存在明显的局部性原理的数据访问模式,LRU表现通常很好。
然而,LRU并不适用于所有场景。在一些对访问时间敏感,但访问频率不高的场景中,LRU可能造成频繁的缓存淘汰和缓存不命中。例如,有些数据可能会在一个长时间周期内反复访问,但在这个周期内的访问频率不高,这种情况下LRU的效果就不如最少使用(LFU)算法。
总的来说,LRU算法适用于访问模式具有明显局部性原理,数据的访问热度变化频率较低的场景。