Redis笔记整理:回收策略与LRU算法
Redis的回收策略
- noeviction:返回错误当内存限制达到并且客户端尝试执行会让更多内存被使用的命令(大部分的写入指令,但DEL和几个例外)
- allkeys-lru: 尝试回收最少使用的键(LRU),使得新添加的数据有空间存放。
- volatile-lru: 尝试回收最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放。
- allkeys-random: 回收随机的键使得新添加的数据有空间存放。
- volatile-random: 回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键。
- volatile-ttl: 回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放。
注意:
如果没有键满足回收的前提条件的话,策略volatile-lru, volatile-random以及volatile-ttl就和noeviction差不多了。
回收策略选择
选择正确的回收策略是非常重要的,这取决于你的应用的访问模式,不过你可以在运行时进行相关的策略调整,并且监控缓存命中率和没命中的次数,通过RedisINFO命令输出以便调优。
一般的经验规则:
- 使用allkeys-lru策略:当你希望你的请求符合一个幂定律分布,也就是说,你希望部分的子集元素将比其它其它元素被访问的更多。如果你不确定选择什么,这是个很好的选择。.
- 使用allkeys-random:如果你是循环访问,所有的键被连续的扫描,或者你希望请求分布正常(所有元素被访问的概率都差不多)。
- 使用volatile-ttl:如果你想要通过创建缓存对象时设置TTL值,来决定哪些对象应该被过期。
注意:
allkeys-lru 和 volatile-random策略对于当你想要单一的实例实现缓存及持久化一些键时很有用。不过一般运行两个实例是解决这个问题的更好方法。
为了键设置过期时间也是需要消耗内存的,所以使用allkeys-lru这种策略更加高效,因为当内存有压力时,没有必要为键去设置过期时间。
回收进程如何工作
理解回收进程如何工作是非常重要的:
- 一个客户端运行了新的命令,添加了新的数据。
- Redis检查内存使用情况,如果大于maxmemory的限制, 则根据设定好的策略进行回收。
- 一个新的命令被执行,等等。
- 所以我们不断地穿越内存限制的边界,通过不断达到边界然后不断地回收回到边界以下。
说明:
Maxmemory:
maxmemory
配置指令用于配置Redis存储数据时指定限制的内存大小。通过redis.conf可以设置该指令,或者之后使用CONFIG SET命令来进行运行时配置。
如果一个命令的结果导致大量内存被使用(例如很大的集合的交集保存到一个新的键),不用多久内存限制就会被这个内存使用量超越。
LRU算法
Redis使用的是近似的LRU算法,通过少量keys进行取样,然后回收其中一个最好的key(被访问时间较早的)。从redis3.0以后,redis改进了回收键的候选池,也就是改善了算法的性能,使得更加近似真的LRU算法。
Redis LRU有个很重要的点,你通过调整每次回收时检查的采样数量,以实现调整算法的精度。这个参数可以通过以下的配置指令调整:maxmemory-samples。
Redis为什么不使用真实的LRU实现是因为这需要太多的内存。不过近似的LRU算法对于应用而言应该是等价的。
java的LRU算法实现
LRU是Least Recently Used的缩写,即最少使用,常用于页面置换算法,为虚拟页式存储管理服务。LRU算法也经常被用作缓存淘汰策略。
方式一:
基于JDK提供的LinkedHashMap的实现。
LinkedHashMap说明:
- LinkedHasnMap内部维护的是双向链表,用来维护插入顺序或LRU顺序。
- 它继承HashMap,具有快速查询的特性。
- 头节点head是最老的节点,尾节点是最近一次被访问的节点,维护LRU顺序时,容量用完后,再执行插入新的数据时,会将头节点移除。
- accessOrder表示维护那种顺序,默认为false。true:代表LRU顺序,false:代表插入顺序。
示例
public class LRUCache<k, v> extends LinkedHashMap<k, v> {
//最多能缓存的数据
private final int CACHE_SIZE;
public LRUCache(int cacheSize) {
//true 表示让linkedHashMap按照访问顺序来进行排序,最近访问的放在头部,最老访问的放在尾部
super((int)Math.ceil(cacheSize / 0.75) + 1, 0.75f, true);
CACHE_SIZE = cacheSize;
}
@Override
protected boolean removeEldestEntry(Map.Entry<k, v> eldest) {
//当map中的数据量大于指定的缓存个数的时候,就自动删除最老的数据
return size() > CACHE_SIZE;
}
}
方式二:
原理:其实根据方式一我们可以推到出,可以使用一个双向链表来缓存数据。
详细逻辑:
- 新的数据插入到链表的头
- 每当链表中的数据被访问,就将该数据移动到链表头
- 当链表容量大于定义容量时,删除链表的尾部数据
示例
/**
* 双向链表
*/
public class Node {
private int key;
private int val;
private Node prev;
private Node next;
}
public class LRUCache {
//容量
private int capacity;
//头节点
private Node head;
//尾节点
private Node last;
private Map<Integer, Node> map;
public LRUCache(int capacity) {
this.capacity = capacity;
map = new HashMap<>(capacity);
}
public int get(int key) {
Node node = map.get(key);
if (node == null) {
return -1;
}
removeHead(node);
return node.val;
}
private void removeHead(Node node) {
if (node == head) {
return;
}
if (node == last) {
last.prev.next = null;
last = last.prev;
} else {
node.prev.next = node.next;
node.next.prev = node.prev;
}
node.prev = head.prev;
node.next = head;
head.prev = node;
head = node;
}
public void put(int key, int value) {
Node node = map.get(key);
if (node == null) {
node = new Node();
node.key = key;
node.val = value;
if (map.size() == capacity) {
removeLast();
}
addHead(node);
map.put(key, node);
} else {
node.val = value;
removeHead(node);
}
}
private void addHead(Node node) {
if (map.isEmpty()) {
head = node;
last = node;
} else {
node.next = head;
head.prev = node;
head = node;
}
}
private void removeLast() {
map.remove(last.key);
Node prev = last.prev;
if (prev != null) {
prev.next = null;
last = prev;
}
}
@Override
public String toString() {
return map.keySet().toString();
}
}
优点:实现逻辑简单
缺点:每次访问都需要更新链表数据
参考:redis官网lru算法
网友评论