Redis 为何使用近似 LRU 算法淘汰数据,而不是真实 LRU?
在《Redis 数据缓存满了怎么办?》我们知道 Redis 缓存满了之后能通过淘汰策略删除数据腾出空间给新数据。
淘汰策略如下所示:

设置过期时间的 key
volatile-ttl、volatile-random、volatile-lru、volatile-lfu 这四种策略淘汰的数据范围是设置了过期时间的数据。
所有的 key
allkeys-lru、allkeys-random、allkeys-lfu 这三种淘汰策略无论这些键值对是否设置了过期时间,当内存不足都会进行淘汰。
这就意味着,即使它的过期时间还没到,也会被删除。当然,如果已经过了过期时间,即使没有被淘汰策略选中,也会被删除。
volatile-ttl 和 volatile-randon 很简单,重点在于 volatile-lru 和 volatile-lfu,他们涉及到 LRU 算法 和 LFU 算法。
今天码哥带大家一起搞定 Redis 的 LRU 算法…
近似 LRU 算法
什么是 LRU 算法呢?
LRU 算法的全程是 Least Rencently Used,顾名思义就是按照最近最久未使用的算法进行数据淘汰。
核心思想「如果该数据最近被访问,那么将来被发放稳的几率也更高」。
我们把所有的数据组织成一个链表:
- MRU:表示链表的表头,代表着最近最常被访问的数据;
- LRU:表示链表的表尾,代表最近最不常使用的数据。

可以发现,LRU 更新和插入新数据都发生在链表首,删除数据都发生在链表尾。
被访问的数据会被移动到 MRU 端,被访问的数据之前的数据则相应往后移动一位。
使用单链表可以么?
如果选用单链表,删除这个结点,需要 O(n) 遍历一遍找到前驱结点。所以选用双向链表,在删除的时候也能 O(1) 完成。
Redis 使用该 LRU 算法管理所有的缓存数据么?
不是的,由于 LRU 算法需要用链表管理所有的数据,会造成大量额外的空间消耗。
除此之外,大量的节点被访问就会带来频繁的链表节点移动操作,从而降低了 Redis 性能。
所以 Redis 对该算法做了简化,Redis LRU 算法并不是真正的 LRU,Redis 通过对少量的 key 采样,并淘汰采样的数据中最久没被访问过的 key。
这就意味着 Redis 无法淘汰数据库最久访问的数据。
Redis LRU 算法有一个重要的点在于可以更改样本数量来调整算法的精度,使其近似接近真实的 LRU 算法,同时又避免了内存的消耗,因为每次只需要采样少量样本,而不是全部数据。
配置如下:
maxmemory-samples 50
运行原理
大家还记得么,数据结构 redisObject 中有一个 lru 字段, 用于记录每个数据最近一次被访问的时间戳。
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
/* LRU time (relative to global lru_clock) or
* LFU data (least significant 8 bits frequency
* and most significant 16 bits access time).
*/
unsigned lru:LRU_BITS;
int refcount;
void *ptr;
} robj;
Redis 在淘汰数据时,第一次随机选出 N 个数据放到候选集合,将 lru 字段值最小的数据淘汰。
当再次需要淘汰数据时,会重新挑选数据放入第一次创建的候选集合,不过有一个挑选标准:进入该集合的数据的 lru 的值必须小于候选集合中最小的 lru 值。
如果新数据进入候选集合的个数达到了 maxmemory-samples 设定的值,那就把候选集合中 lru 最小的数据淘汰。
这样就大大减少链表节点数量,同时不用每次访问数据都移动链表节点,大大提升了性能。
Java 实现 LRU Cahce
LinkedHashMap 实现
完全利用 Java 的LinkedHashMap实现,可以采用组合或者继承的方式实现,「码哥」使用组合的形式完成。
public class LRUCache<K, V> {
private Map<K, V> map;
private final int cacheSize;
public LRUCache(int initialCapacity) {
map = new LinkedHashMap<K, V>(initialCapacity, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > cacheSize;
}
};
this.cacheSize = initialCapacity;
}
}
重点在于 LinkedHashMap的第三个构造函数上,要把这个构造参数accessOrder设为true,代表LinkedHashMap内部维持访问顺序。
另外,还需要重写removeEldestEntry(),这个函数如果返回true,代表把最久未被访问的节点移除,从而实现淘汰数据。
自己实现
其中代码是从 LeetCode 146. LRU Cache 上摘下来的。代码里面有注释。
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 在链头放最久未被使用的元素,链尾放刚刚添加或访问的元素
*/
class LRUCache {
class Node {
int key, value;
Node pre, next;
Node(int key, int value) {
this.key = key;
this.value = value;
pre = this;
next = this;
}
}
private final int capacity;// LRU Cache的容量
private Node dummy;// dummy节点是一个冗余节点,dummy的next是链表的第一个节点,dummy的pre是链表的最后一个节点
private Map<Integer, Node> cache;//保存key-Node对,Node是双向链表节点
public LRUCache(int capacity) {
this.capacity = capacity;
dummy = new Node(0, 0);
cache = new ConcurrentHashMap<>();
}
public int get(int key) {
Node node = cache.get(key);
if (node == null) return -1;
remove(node);
add(node);
return node.value;
}
public void put(int key, int value) {
Node node = cache.get(key);
if (node == null) {
if (cache.size() >= capacity) {
cache.remove(dummy.next.key);
remove(dummy.next);
}
node = new Node(key, value);
cache.put(key, node);
add(node);
} else {
cache.remove(node.key);
remove(node);
node = new Node(key, value);
cache.put(key, node);
add(node);
}
}
/**
* 在链表尾部添加新节点
*
* @param node 新节点
*/
private void add(Node node) {
dummy.pre.next = node;
node.pre = dummy.pre;
node.next = dummy;
dummy.pre = node;
}
/**
* 从双向链表中删除该节点
*
* @param node 要删除的节点
*/
private void remove(Node node) {
node.pre.next = node.next;
node.next.pre = node.pre;
}
}
不要吝啬赞美,当别人做的不错,就给予他正反馈。少关注用「赞美」投票的事情,而应该去关注用「交易」投票的事情。
判断一个人是否牛逼,不是看网上有多少人夸赞他,而是要看有多少人愿意跟他发生交易或赞赏、支付、下单。
因为赞美太廉价,而愿意与他发生交易的才是真正的信任和支持。
码哥到现在已经写了近 23+ 篇 Redis 文章,赠送了很多书籍,收到过许多赞美和少量赞赏,感谢曾经赞赏过我的读者,谢谢。
我是「码哥」,大家可以叫我靓仔,好文请点赞,关于 LFU 算法,我们下一篇见。
历史好文
参考文献
https://redis.io/docs/manual/eviction/
https://time.geekbang.org/column/article/294640
https://halfrost.com/lru_lfu_interview/
https://blog.csdn.net/csdlwzy/article/details/95635083
Redis 为何使用近似 LRU 算法淘汰数据,而不是真实 LRU?的更多相关文章
- 动手实现 LRU 算法,以及 Caffeine 和 Redis 中的缓存淘汰策略
我是风筝,公众号「古时的风筝」. 文章会收录在 JavaNewBee 中,更有 Java 后端知识图谱,从小白到大牛要走的路都在里面. 那天我在 LeetCode 上刷到一道 LRU 缓存机制的问题, ...
- Redis内存管理中的LRU算法
在讨论Redis内存管理中的LRU算法之前,先简单说一下LRU算法: LRU算法:即Least Recently Used,表示最近最少使用页面置换算法.是为虚拟页式存储管理服务的,是根据页面调入内存 ...
- Redis内存回收:LRU算法
Redis技术交流群481804090 Redis:https://github.com/zwjlpeng/Redis_Deep_Read Redis中采用两种算法进行内存回收,引用计数算法以及LRU ...
- 最近最久未使用页面淘汰算法———LRU算法(java实现)
请珍惜小编劳动成果,该文章为小编原创,转载请注明出处. LRU算法,即Last Recently Used ---选择最后一次访问时间距离当前时间最长的一页并淘汰之--即淘汰最长时间没有使用的页 按照 ...
- 算法之如何实现LRU缓冲淘汰策略
1)什么是缓存? 缓存是一种提高数据读取性能的技术,在硬件设计.软件开发中都有着非广泛的应用,比如常见的CPU缓存.数据库缓存.浏览器缓存等等. 2)为什么使用缓存?即缓存的特点缓存的大小是有限的,当 ...
- LRU算法原理解析
LRU是Least Recently Used的缩写,即最近最少使用,常用于页面置换算法,是为虚拟页式存储管理服务的. 现代操作系统提供了一种对主存的抽象概念虚拟内存,来对主存进行更好地管理.他将主存 ...
- redis数据结构、持久化、缓存淘汰策略
Redis 单线程高性能,它所有的数据都在内存中,所有的运算都是内存级别的运算,而且单线程避免了多线程的切换性能损耗问题.redis利用epoll来实现IO多路复用,将连接信息和事件放到队列中,依次放 ...
- 缓存算法(页面置换算法)-FIFO、LFU、LRU
在前一篇文章中通过leetcode的一道题目了解了LRU算法的具体设计思路,下面继续来探讨一下另外两种常见的Cache算法:FIFO.LFU 1.FIFO算法 FIFO(First in First ...
- 如何实现LRU算法?
1.什么是LRU算法? LRU是一种缓存淘汰机制策略. 计算机的缓存容量有限,如果缓存满了就要删除一些内容,给新的内容腾位置.但是要删除哪些内容呢?我们肯定希望删掉那些没有用的缓存,而把有用的数据继续 ...
随机推荐
- 学习ELK日志平台(一)
一.需求及基础: 场景: 1.开发人员不能登录线上服务器查看详细日志 2.各个系统都有日志,日志数据分散难以查找 3.日志数据量大,查询速度慢,或者数据不够实时 4.一个调用会涉及到多个系统,难以在这 ...
- Tomcat配置文件之“server.xml”解析
<?xml version="1.0" encoding="UTF-8"?> <!--代表整个Servlet容器组件,是最顶层元素.它里面可以 ...
- servlet中的ServletConfig对象
ServletConfig对象对应web.xml文件中的<servlet>节点.当Tomcat初始化一个Servlet时,会创建ServletConfig对象,并将该Servlet的配置信 ...
- 10.Flink实时项目之订单维度表关联
1. 维度查询 在上一篇中,我们已经把订单和订单明细表join完,本文将关联订单的其他维度数据,维度关联实际上就是在流中查询存储在 hbase 中的数据表.但是即使通过主键的方式查询,hbase 速度 ...
- C语言小游戏——2048
2048 2048这款游戏的玩法很简单,每次可以选择上下左右滑动,每滑动一次,所有的数字方块都会往滑动的方向靠拢,系统也会在空白的地方乱数出现一个数字方块,相同数字的方块在靠拢.相撞时会相加. ...
- 技术架构:IT生存之道
Technical architecture: What IT does for a living (cio.com) Technical architecture: What IT does for ...
- 原型模式故事链--new一个对象的过程
上一个总标题:https://segmentfault.com/a/11...提问:你有对象了吗?答:没有.笨!new一个不就好了吗! 问题点:为什么我要理解new一个对象的过程?答:不理解这个过程, ...
- ES6-11学习笔记--数组的扩展
类数组 / 伪数组 Array.from() Array.of() copyWithin() fill() includes() 类数组.伪数组例子: let divs = document.ge ...
- PAT B1086 就不告诉你
题目描述: 做作业的时候,邻座的小盆友问你:"五乘以七等于多少?"你应该不失礼貌地围笑着告诉他:"五十三."本题就要求你,对任何一对给定的正整数,倒着输出它们的 ...
- java中"Static块"是怎么回事,怎么用的,有什么意义
6.Static块 Static块:该类的任何方法被首次触碰到时(马克-to-win: when you touch Test的main方法时),Static块被运行.可以在里面初始化你的stati ...