深入了解Redis内存淘汰策略中的LRU算法应用

LRU算法简析

LRU(Least Recently Used,最近最少使用)算法是一种常见的内存淘汰策略,它根据数据的访问时间来决定哪些数据会被淘汰。LRU算法的核心思想是:最久未被访问的数据,被认为是最不常用的数据,应该被优先淘汰。

LRU 算法广泛应用在诸多系统内,例如 Linux 内核页表交换,MySQL Buffer Pool 缓存页替换,以及 Redis 数据淘汰策略。

简单来说,可以认为LRU算法是在维护一个链表,每次操作某个数据,就把数据移到链表的头部。

例如:

  1. 向一个缓存空间依次插入三个数据 A/B/C,填满了缓存空间;

  2. 读取数据 A 一次,按照访问时间排序,数据 A 被移动到缓存头部;

  3. 插入数据 D 的时候,由于缓存空间已满,触发了 LRU 的淘汰策略,数据 B 被移出,缓存空间只保留了 D/A/C。
    在这里插入图片描述

但是一般而言,LRU 算法的数据结构不会如上边那样,仅使用简单的队列或链表去缓存数据,而是会采用 Hash 表 + 双向链表的结构类似于hsahMap的结构,利用 Hash 表确保数据查找的时间复杂度是 O(1),双向链表又可以使数据插入 / 删除等操作也是 O(1)。

Redis中的LRU算法

Redis中为何使用近似LRU算法

按照官方文档的介绍,Redis 所实现的是一种近似的 LRU 算法。

因为若严格按LRU实现,假设Redis保存的数据较多,还要在代码中实现为:

Redis使用最大内存时,可容纳的所有数据维护一个链表需额外内存空间来保存链表每当有新数据插入或现有数据被再次访问,需执行多次链表操作在访问数据的过程中,让Redis受到数据移动和链表操作的开销影响,最终导致降低Redis访问性能。所以,无论是为节省内存 or 保持Redis高性能,Redis并未严格按LRU基本原理实现,而是提供了一个近似LRU算法实现。

LRU算法要求删除最近最少使用的kv,所以redis需要维护每一个kv的使用时间来判断数据访问的时效性。这就是LRU 时钟。

LRU 时钟

LRU时钟:记录数据每次访问的时间戳。

下面的这段代码是获取LRU时钟的方法接口:

#define LRU_BITS 24
#define LRU_CLOCK_MAX ((1<<LRU_BITS)-1)
#define LRU_CLOCK_RESOLUTION 1000unsigned int getLRUClock(void) {return (mstime()/LRU_CLOCK_RESOLUTION) & LRU_CLOCK_MAX;
}

通过当前的 unix 时间戳获取 LRU 时钟。unix 时间戳通过接口 mstime ()获取,得到的是从 1970年1月1日早上8点到当前时刻的时间间隔,以毫秒为单位(mstime底层实现用的是 c 的系统函数 gettimeofday)。
其中,LRU_BITS 表示 LRU 时钟的位数;
LRU_CLOCK_MAX 为 LRU 时钟的最大值;LRU_CLOCK_RESOLUTION 则表示每个 LRU 基本单位对应到自然时钟的毫秒数,即精度,按照这个宏定义,LRU 时钟的最小刻度为 1000 毫秒。

将自然时钟和 LRU 时钟作对比:

    a)自然时钟最大值为 11:59:59,LRU 时钟最大值为 LRU_CLOCK_MAX = 2^24 - 1;b)自然时钟的最小刻度为 1秒, LRU 时钟的最小刻度为 1000 毫秒; 		c)自然时钟的一个轮回是 12小时,LRU 时钟的一个轮回是 2^24 * 1000 毫秒(一轮的计算方式是:( 时钟最大值 + 1 ) * 最小刻度);

因为 LRU_CLOCK_MAX 是 2 的幂减 1,即它的二进制表示全是 1,所以这里的 & 其实是取模的意思。
那么 getLRUClock 函数的含义就是定位到 LRU 时钟的某个刻度。

Redis中的LRU时钟

在redis中,引入了LRU 时钟来记录使用时间,每个对象的每次被访问都会记录下当前服务器的 LRU 时钟,然后用服务器的 LRU 时钟减去对象本身的时钟,得到的就是这个对象没有被访问的时间间隔(也称空闲时间),空闲时间最大的就是需要淘汰的对象。

具体实现:

Redis Server会使用一个实例级别的全局LRU时钟,每个KV对的LRU time会根据全局LRU时钟进行设置

1、Redis 对象(数据结构)

Redis 中的所有对象定义为 redisObject 结构体,也正是这些对象采用了 LRU 算法进行内存回收,所以每个对象需要一个成员来用来记录该对象的最近一次被访问的时间(即 lru 成员),由于时钟的最大值只需要 24 个比特位就能表示,所以结构体定义时采用了位域。定义如下:

typedef struct redisObject {unsigned type:4;unsigned encoding:4;unsigned lru:LRU_BITS;int refcount;void *ptr;
} robj;
2、Redis 定时器(全局LRU时钟)

Redis 中有一个全局的定时器函数 serverCron,用于刷新服务器的 LRU 时钟,函数大致实现如下:

int serverCron(...) {...server.lruclock = getLRUClock();...
}
3、Redis 对象的 LRU 时钟(根据全局LRU时钟进行设置)

每个 Redis 对象的 LRU 时钟的计算方式由 LRU_CLOCK 给出,实现如下:

#define LRU_CLOCK() ((1000/server.hz <= LRU_CLOCK_RESOLUTION) ? server.lruclock : getLRUClock())

server.lruclock 代表服务器的 LRU 时钟

server.hz 决定这个时钟的刷新频率,即每秒钟会调用 server.hz 次 serverCron 函数。默认值为 10。

那么,服务器每 1 / server.hz 秒就会调用一次定时器函数 serverCron。

1 / server.hz 代表了 serverCron 这个定时器函数两次调用之间的最小时间间隔(以秒为单位),那么 1000 / server.hz 就是以毫秒为单位的定时器函数两次调用之间的最小时间间隔。

以上代码的逻辑就是:如果这个最小时间间隔小于等于 LRU 时钟的精度,那么不需要重新计算 LRU时钟,直接用服务器 LRU时钟做近似值即可。

因为时间间隔越小,server.lruclock 刷新的越频繁;相反,当时间间隔很大的时候,server.lruclock 的刷新可能不及时,所以需要用 getLRUClock 重新计算准确的 LRU 时钟。

4、Redis 对象更新 LRU 时钟时机

Redis 对象更新 LRU 时钟的地方有两个:

a)     对象创建时;
b)     对象被使用时;
a) 对象创建时

createObject 函数用于创建一个 Redis 对象,代码实现在 object.c 中:

robj *createObject(int type, void *ptr) {robj *o = zmalloc(sizeof(*o));o->type = type;o->encoding = OBJ_ENCODING_RAW;o->ptr = ptr;o->refcount = 1;o->lru = LRU_CLOCK();return o;
}
b) 对象被使用时

lookupKey 不会直接被 redis 命令调用,往往是通过lookupKeyRead()、lookupKeyWrite() 、lookupKeyReadWithFlags() 间接调用的,这个函数的作用是通过传入的 key 查找对应的 redis 对象,并且会在条件满足时设置上 LRU 时钟。

这是简化代码:

robj *lookupKey(redisDb *db, robj *key, int flags) {dictEntry *de = dictFind(db->dict,key->ptr);if (de) {robj *val = dictGetVal(de);...val->lru = LRU_CLOCK();...return val;} else {return NULL;}
}

Redis 中的 LRU 内存回收 (内存淘汰)

LRU 淘汰策略配置
maxmemory 1073741824
maxmemory-policy allkeys-lru
maxmemory-samples 5

这三个配置项决定了 Redis 内存回收时的机制。

maxmemory 指定了内存使用的极限,以字节为单位。当内存达到极限时,他会尝试去删除一些键值。
设置maxmemory时,如果你的 Redis 是主 Redis 时 (Redis 采用主从模式时),需要预留一部分系统内存给同步队列缓存。

maxmemory-policy 配置来指定删除的策略。如果根据指定的策略无法删除键或者策略本身就是 ‘noeviction’,那么,Redis 会根据命令的类型做出不同的回应:会给需要更多内存的命令返回一个错误,例如 SET、LPUSH 等等;而像 GET 这样的只读命令则可以继续正常运行。

maxmemory-samples :指定了在进行删除时的键的采样数量。LRU 和 TTL 都是近似算法,所以可以根据参数来进行取舍,到底是要速度还是精确度。默认值一般填 5。10 的话已经非常近似正式的 LRU 算法了,但是会多一些 CPU 消耗;3 的话执行更快,然而不够精确。

空闲时间

LRU 算法的执行依据是将空闲时间最大的淘汰掉,每个对象知道自己上次使用的时间,那么就可以计算出自己空闲了多久,可以通过 estimateObjectIdleTime 接口得出 idletime.

unsigned long long estimateObjectIdleTime(robj *o) {unsigned long long lruclock = LRU_CLOCK();if (lruclock >= o->lru) {return (lruclock - o->lru) * LRU_CLOCK_RESOLUTION;} else {return (lruclock + (LRU_CLOCK_MAX - o->lru)) * LRU_CLOCK_RESOLUTION;}
}

从代码可以看出,因为LRU时钟只有24位,所以LRU时钟最大值是LRU_CLOCK_MAX,也就是2^24 - 1,换算下大概是190天左右,所以LRU时钟大概是190天一轮,当某个kv的LRU时钟比全局LRU时钟还要大时,说明kv已经超过190天没使用,这时他的 空闲时间是对象的LRU时钟加上全局的LRU时钟。

近似LRU具体执行过程
过程简述

Redis 的数据库是一个巨大的字典,最上层是由键值对组成的。当内存使用超过最大使用数时,就需要采用回收策略进行内存回收。

如果回收策略采用 LRU,那么就会在这个大字典里面随机采样,挑选出空闲时间最大的键进行删除。

而回收池会存在于整个服务器的生命周期中,所以它是一个全局变量。

近似LRU过程详解
  1. 删除操作发生在每一次处理客户端命令时。当 server.maxmemory 的值非 0,则检测是否有需要回收的内存。如果有则执行 2) ;

  2. 随机从大字典中取出 server.maxmemory_samples 个键(实际取到的数量取决于大字典原本的大小),然后用一个长度为 16 (由 MAXMEMORY_EVICTION_POOL_SIZE 指定) 的 evictionPool (回收池)对这几个键进行筛选,筛选出 idletime (空闲时间)最长的键,并且按照 idletime 从小到大的顺序排列在 evictionPool 中;

  3. 从 evictionPool 池中取出 idletime 最大且在字典中存在的键作为 bestkey 执行删除,并且从 evictionPool 池中移除;

LRU 回收算法的实际执行流程
//eviction_pool数组长度
#define MAXMEMORY_EVICTION_POOL_SIZE 16
struct evictionPoolEntry {                                   //空闲时间unsigned long long idle;//键名keysds key;
};
int processCommand(client *c) {...if (server.maxmemory) freeMemoryIfNeeded();                    ...
}//收集 evictionPool 元素并且找出空闲时间最大的键并进行释放;
int freeMemoryIfNeeded(void) {...if (server.maxmemory_policy == MAXMEMORY_ALLKEYS_LRU ||server.maxmemory_policy == MAXMEMORY_VOLATILE_LRU) {//eviction_pool 是数据库对象 db 的成员,代表回收池,是evictionPoolEntry 类型的数组,数组长度由MAXMEMORY_EVICTION_POOL_SIZE 指定,默认值为 16;struct evictionPoolEntry *pool = db->eviction_pool;      while(bestkey == NULL) {//evictionPoolPopulate(...) 接口用于随机采样数据库中的键,并且逐一和回收池中的键的空闲时间进行比较,筛选出空闲时间最大的键留在回收池中evictionPoolPopulate(dict, db->dict, db->eviction_pool);   for (k = MAXMEMORY_EVICTION_POOL_SIZE-1; k >= 0; k--) {if (pool[k].key == NULL) continue;de = dictFind(dict,pool[k].key);sdsfree(pool[k].key);memmove(pool+k,pool+k+1,sizeof(pool[0])*(MAXMEMORY_EVICTION_POOL_SIZE-k-1));pool[MAXMEMORY_EVICTION_POOL_SIZE-1].key = NULL;pool[MAXMEMORY_EVICTION_POOL_SIZE-1].idle = 0;if (de) {//找出空闲时间最大且存在的键,等待执行删除操作bestkey = dictGetKey(de);                        break;} else {continue;}}}}...
}
回收池更新详解(evictionPoolPopulate)—LRU 算法的核心
#define EVICTION_SAMPLES_ARRAY_SIZE 16
void evictionPoolPopulate(dict *sampledict, dict *keydict, struct evictionPoolEntry *pool) {int j, k, count;dictEntry *_samples[EVICTION_SAMPLES_ARRAY_SIZE];dictEntry **samples;if (server.maxmemory_samples <= EVICTION_SAMPLES_ARRAY_SIZE) {samples = _samples;} else {samples = zmalloc(sizeof(samples[0])*server.maxmemory_samples);}count = dictGetSomeKeys(sampledict,samples,server.maxmemory_samples);for (j = 0; j < count; j++) {unsigned long long idle;sds key;robj *o;dictEntry *de;de = samples[j];key = dictGetKey(de);if (sampledict != keydict) de = dictFind(keydict, key);o = dictGetVal(de);idle = estimateObjectIdleTime(o);k = 0;while (k < MAXMEMORY_EVICTION_POOL_SIZE &&pool[k].key &&pool[k].idle < idle) k++;if (k == 0 && pool[MAXMEMORY_EVICTION_POOL_SIZE-1].key != NULL) {continue;                                                           /* a */} else if (k < MAXMEMORY_EVICTION_POOL_SIZE && pool[k].key == NULL) {   /* b */} else {if (pool[MAXMEMORY_EVICTION_POOL_SIZE-1].key == NULL) {             /* c */memmove(pool+k+1,pool+k,sizeof(pool[0])*(MAXMEMORY_EVICTION_POOL_SIZE-k-1));} else {k--;                                                            /* d */sdsfree(pool[0].key);memmove(pool,pool+1,sizeof(pool[0])*k);}}pool[k].key = sdsdup(key);pool[k].idle = idle;}if (samples != _samples) zfree(samples);
}

这是 LRU 算法的核心,首先从目标字典中随机采样出 server.maxmemory_samples 个键,缓存在 samples 数组中,然后一个一个取出来,并且和回收池中的已有的键对比空闲时间,从而更新回收池。更新的过程首先,利用遍历找到每个键的实际插入位置 k ,然后,总共涉及四种情况如下:

   a) 回收池已满,且当前插入的元素的空闲时间最小,则不作任何操作;b) 回收池未满,且将要插入的位置 k 原本没有键,则可直接执行插入操作;c) 回收池未满,且将要插入的位置 k 原本已经有键,则将当前第 k 个以后的元素往后挪一个位置,然后执行插入操作;d) 回收池已满,则将当前第 k 个以前的元素往前挪一个位置,然后执行插入操作;

Redis为何使用近似LRU算法

筛选规则,Redis 是随机抽取一批数据去按照淘汰策略排序,不再需要对所有数据排序;

性能问题,每次数据访问都可能涉及数据移位,性能会有少许损失;

内存问题,Redis 对内存的使用一向很 “抠门”,数据结构都很精简,尽量不使用复杂的数据结构管理数据;

策略配置,如果线上 Redis 实例动态修改淘汰策略会触发全部数据的结构性改变,这个 Redis 系统无法承受的。

致谢

部分内容援引地址:
英雄哪里出来 https://blog.csdn.net/WhereIsHeroFrom/article/details/86501571/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/2564.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于Tensorflow完成mnist数据集的数字手写体识别

基于Tensorflow完成mnist数据集的数字手写体识别 关于知识背景CNNFCNN 关于数据集新的改变 关于知识背景 CNN 卷积神经网络&#xff08;Convolutional Neural Networks&#xff0c;简称CNN&#xff09;是一种具有局部连接、权值共享等特点的深层前馈神经网络&#xff08;Feed…

【大数据】LSM树,专为海量数据读写而生的数据结构

目录 1.什么是LSM树&#xff1f; 2.LSM树的落地实现 1.什么是LSM树&#xff1f; LSM树&#xff08;Log-Structured Merge Tree&#xff09;是一种专门针对大量写操作做了优化的数据存储结构&#xff0c;尤其适用于现代大规模数据处理系统&#xff0c;如NoSQL数据库&#xff…

C# winform OpenProtocol中数据中的UI是什么类型?

C# winform OpenProtocol中数据中的UI是什么类型&#xff1f;

vue2项目升级到vue3经历分享

依据vue官方文档&#xff0c;vue2在2023年12月31日终止维护。因此决定将原来的岁月云记账升级到vue3&#xff0c;预计工作量有点大&#xff0c;于是想着把过程记录下来。 原系统使用的技术栈 "dependencies": {"axios": "^0.21.1","babel-…

C++-DAY1

思维导图 有以下定义&#xff0c;说明哪些量可以改变哪些不可以改变&#xff1f; const char *p; const (char *) p; char *const p; const char* const p; char const *p; (char *) const p; char const* const p; const char *p&#xff1a;指针 p 所指向的内容不可改…

【嵌入式】Arduino IDE + ESP32开发环境配置

一 背景说明 最近想捣鼓一下ESP32的集成芯片&#xff0c;比较了一下&#xff0c;选择Arduino IDE并添加ESP32支持库的方式来开发&#xff0c;下面记录一下安装过程以及安装过程中遇到的坑。 二 下载准备 【1】Arduino IDE ESP32支持一键安装包&#xff08;非常推荐&#xff0…

如何将web content项目导入idea并部署到tomcat

将Web Content项目导入IntelliJ IDEA并部署到Tomcat主要涉及以下几个步骤&#xff1a; 1. 导入Web Content项目 打开IntelliJ IDEA。选择“File” -> “New” -> “Project from Existing Sources…”。浏览到你的Web Content项目的文件夹&#xff0c;并选择它。Intell…

Spring的9个核心功能(一)

目录 资源管理 Java资源管理 1、来个Demo 2、原理 Spring资源管理 1、资源抽象 Resource WritableResource 2、资源加载 3、小结 环境 1、Environment 2、配置属性源PropertySource 3、SpringBoot是如何解析配置文件 类型转换 1、类型转换API …

什么是IIoT?

什么是IIoT? IIoT,即工业物联网(Industrial Internet of Things),是指将物联网技术应用到工业领域,通过微型低成本传感器、高带宽无线网络等技术手段,实现工业设备、系统和服务的互联互通,从而提高生产效率、降低能耗和成本,实现智能化和自动化生产。 IIoT的应用范围…

网络安全是否有需求

● 由于网络威胁数量不断增加&#xff0c;网络安全的需求很高。 ● 组织正在大力投资网络安全以保护其数据。 ● 就业市场缺乏熟练的网络安全专业人员。 ● 网络安全认证可以提升您在网络安全领域的职业前景。 ● 持续学习并了解最新的安全趋势在该领域至关重要。 随着对技术和…

vue3去掉el-table底部白色边框

加入下面这一行代码就行了&#xff0c;我用的是less :deep(.el-table__inner-wrapper:before) {background: none;}效果图

使用PyCharm开发工具创建工程

一. 简介 前面文章实现了开发 python程序使用的 开发工具PyCharm&#xff0c;本文来学习使用 PyCharm开发工具创建一个 python工程。 二. 使用PyCharm开发工具创建工程 1. 首先&#xff0c;打开 PyCharm开发工具&#xff0c;打开 "New project" 选项&#xff1a; …

详解数据结构:队列(含栈与队列扩展)

一、顺序队列 有一种线性序列&#xff0c;特点是先进先出&#xff0c;这种存储结构称为队列。队列也是一种线性表&#xff0c;只不过它是操作受限的线性表&#xff0c;只能再两端操作&#xff1a;一端进、一端出。进的一端称为队尾&#xff0c;出的一端称为队头。队列可以用顺…

20240424codeforces刷题题解

240424刷题题解 Walk on Matrix CodeForces - 1332D 思路 构造题&#xff0c;每个 d p i , j dp_{i,j} dpi,j​​​都是由其左上方向中的按位与最大值决定的。 我们需要从使得贪心解与正确解的差值为 k k k。 为了方便获得 k k k&#xff0c;可以考虑构造一个贪心解为 0…

Windows批处理脚本,用于管理Nginx服务器

先看截图&#xff1a; Windows批处理脚本&#xff0c;用于管理Nginx服务器。它提供了启动、重启、关闭Nginx以及刷新控制台等功能。 设置环境变量&#xff1a; set NGINX_PATHD:&#xff1a;设置Nginx所在的盘符为D盘。set NGINX_DIRD:\nginx1912\&#xff1a;设置Nginx所在…

HTML5+CSS3小实例:炫彩荧光线条登录框

实例:炫彩荧光线条登录框 技术栈:HTML+CSS 效果: 源码: 【HTML】 <!DOCTYPE html> <html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-sca…

每日一题---环形链表的约瑟夫问题

文章目录 前言1.题目2.解题思路2.1创建节点 2.2.创建环形链表2.3.进行遍历 4参考代码 前言 前段时间我们学习到了单链表和双向链表的相关知识&#xff0c;下面我们解决一道具有代表性的一个编程题。 牛客网—环形链表的约瑟夫问题 1.题目 2.解题思路 2.1创建节点 //创建节点…

scratch选择火车下铺 2024年3月中国电子学会图形化编程 少儿编程 scratch编程等级考试四级真题和答案解析

目录 scratch根据身份证号码识别是否优先选择火车下铺 一、题目要求 1、准备工作 2、功能实现 二、案例分析

25计算机考研院校数据分析 | 复旦大学

复旦大学(fudan University)&#xff0c;简称"复旦”&#xff0c;位于中国上海&#xff0c;由中华人民共和国教育部直属&#xff0c;中央直管副部级建制&#xff0c;位列985工程、211工程、双一流A类&#xff0c;入选“珠峰计划"、"111计划""2011计划…

【学习】软件测试自动化,是未来的趋势还是当前的必需

在当今快速迭代的软件开发周期中&#xff0c;速度和质量成为了企业生存的关键。随着DevOps实践的普及和持续集成/持续部署&#xff08;CI/CD&#xff09;流程的标准化&#xff0c;软件测试自动化已经从未来的趋势转变为当前的必要性。本文将探讨自动化测试的现状、必要性以及其…