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

news2024/12/23 10:10:17

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 1000
 
unsigned 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;
    //键名key
    sds 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.coloradmin.cn/o/1622374.html

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

相关文章

UE5 GAS开发P41-43 永久效果,去除永久效果,伤害区域,EnumClass,开始重叠与结束重叠事件

这一部分学习了怎么创建一个伤害性的地形(火焰地形,毒沼泽等都可以用这个方式创建) AuraEffectActor.h // Fill out your copyright notice in the Description page of Project Settings.#pragma once#include "CoreMinimal.h" #include "GameplayEffect.h&q…

Linux驱动开发:掌握SPI通信机制

目录标题 1、SPI简介2、SPI通信机制3、Linux内核中的SPI支持4、SPI核心API5、SPI控制器驱动6、SPI设备驱动 7、编写SPI设备驱动8、调试SPI驱动 在Linux驱动开发中&#xff0c;串行外设接口(SPI)是一种常见的高速全双工通信协议&#xff0c;用于连接处理器和各种外设。本文将深入…

React【Day4下+5】

环境搭建 使用CRA创建项目&#xff0c;并安装必要依赖&#xff0c;包括下列基础包 Redux状态管理 - reduxjs/toolkit 、 react-redux路由 - react-router-dom时间处理 - dayjsclass类名处理 - classnames移动端组件库 - antd-mobile请求插件 - axios pnpm i reduxjs/toolkit r…

企业规模扩大,SD-WAN实现跨省快速组网

随着数字化时代的飞速发展&#xff0c;企业面临着前所未有的挑战与机遇。5G、VoIP、AI和物联网等新技术的兴起&#xff0c;不仅改变了商业格局&#xff0c;也对企业网络提出了更高的要求。随着企业规模的不断扩大&#xff0c;企业如何搭建跨省的、高性能、超融合、简化运维的组…

防火墙技术基础篇:认识安全策略、安全区域、域间转发及报文转发流程

防火墙技术基础篇&#xff1a;认识安全策略、安全区域、域间转发及报文转发流程 一、安全策略匹配机制 简单通俗的讲&#xff0c;防火墙设备最基本的用途就是定义数据如何转发&#xff0c;靠什么定义呢&#xff1f;最基本的就是安全策略&#xff0c;当流量来到防火墙之后首先…

LeetCode_链表的回文结构

✨✨所属专栏&#xff1a;LeetCode刷题专栏✨✨ ✨✨作者主页&#xff1a;嶔某✨✨ 题目描述&#xff1a; 对于一个链表&#xff0c;请设计一个时间复杂度为O(n),额外空间复杂度为O(1)的算法&#xff0c;判断其是否为回文结构。给定一个链表的头指针A&#xff0c;请返回一个bo…

Edge下载文件提示无法安全下载的解决方法

问题描述&#xff1a;最近Edge在下载文件时总是提示&#xff1a;无法安全下载&#xff0c;本文记录一下解决方法。 提示截图&#xff1a; 解决方式一&#xff1a; 1. 点击下图红框的三个点&#xff0c;选择保留 2. 选择仍然保留 解决方式二&#xff1a; 第一种方式每下载一次…

微信小程序中,plugins 配置项如何配置多个插件

在微信小程序中&#xff0c;如果需要配置多个插件&#xff0c;你可以在 app.json 文件的 plugins 配置项中为每个插件指定一个唯一的自定义名称&#xff0c;并分别提供它们的 version 和 provider 信息。下面是一个配置多个插件的示例&#xff1a; json复制代码 { "pages…

Python 0基础_变现_38岁_day 16(文件操作)

在python&#xff0c;使用内置函数open()进行文件的一些读写操作 文件操作格式&#xff1a;open(文件路径&#xff0c;访问模式&#xff0c;字符编码) 前面两个参数是必备参数&#xff0c;后面的字符编码为选填&#xff0c;但是大多数情况下都会协商字符编码 访问模式 r 只读 w…

如何看待AIGC技术

目录 1.概述 2.技术应用 2.1.媒体与内容创作 2.2.教育与学习 ​​​​​​​2.3.艺术创作 ​​​​​​​2.4.游戏产业 ​​​​​​​2.5.工业设计 ​​​​​​​2.6.对未来社会的影响 2.7.可能的发展方向 ​​​​​​​2.8.小结 3.伦理与风险 3.1.AIGC技术面临…

科研工作学习中常用的录制动图软件——screenToGif

一、前言 俗话说&#xff0c;字不如表&#xff0c;表不如图&#xff0c;静图不如动图。 动图给人的直观感受&#xff0c;还是很不错的。在曾经的学生期间&#xff0c;进行组会汇报&#xff1b;还是如今工作中&#xff0c;给领导汇报。我经常使用screenToGif这款软件&#xff…

基于STM32F4系列的ETH IAP在线升级程序

目录 1、前言 2、以太网的移植&#xff08;无操作系统&#xff09; 3、移植FATS 系统 4、移植ETH 驱动及 DP83848驱动 5、Tftp 服务程序 6、注意事项 ​7、代码 资料下载地址&#xff1a;基于STM32F4系列的ETH IAP在线升级程序 1、前言 此bootloader程序可以通过http…

数字科技助力垃圾分类展厅,增强内容交互新体验!

如今&#xff0c;许多行业都开始运用数字技术&#xff0c;探索其在展览展示领域中的应用&#xff0c;其中垃圾分类展厅作为现代城市文明建设的重要一环&#xff0c;也通过这些技术的运用&#xff0c;打造出了更加生动且富有科技感的展示空间&#xff0c;它不仅提升公众对垃圾分…

TCP详解

2.1TCP 由IETF的RFC793定义的传输控制协议&#xff08;Transmission Control Protocol&#xff0c;TCP&#xff09;是一种基于字节流的传输层通信协议。在传输数据前需要在发送与接收者之间建立连接&#xff0c;通过相应机制保证其建立连接的可靠性。 TCP协议具备以下特性&am…

信息化工作人员必备常识8——DNS缓存【查看缓存与清理dns缓存】

信息化工作人员必备常识8——DNS缓存【查看缓存与清理dns缓存】 前言信息化人员必备常识回顾&#xff08;不想看回顾的直接往下滑哦~&#xff09;pingtelnetnslookup命令ipconfig命令域名DNS DNS缓存主机上解析DNS的优先级 清理DNS缓存查看当前电脑有哪些DNS缓存 前言 信息化相…

【超详细保姆教程】手把手教你如何修改el-tab的默认样式,把el-tab的label变成我们想要的

目录 前言实现效果官方的样式修改后的结果 步骤1、去掉分割线和下划线去掉前去掉后 2、设置插槽&#xff0c;自定义label的内容&#xff0c;加上border等结果 3、为选中的tab修改高亮结果 前言 做前端实习也有一段时间了&#xff0c;昨天晚上在学校上项目工程管理这门课的时候…

Edge浏览器下载文件提示 “无法安全下载” 的解决方法

提示如下&#xff1a; 虽然我们可以通过 "保留" 进行下载&#xff0c;但是每次需要选择&#xff0c;比较麻烦 解决方法&#xff1a; 1、打开注册表 HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft 2、创建2个 "项" Edge\InsecureContentAllowedForUrls…

数据链路层(计算机网络,待完善)

0、前言 本文大多数图片都来自于 B站UP主&#xff1a;湖科大教书匠 的教学视频&#xff0c;对高军老师及其团队制作出这么优质的课程表示感谢。文章增加了部分个人理解&#xff0c;内容并不是对视频的静态化翻译。 1、概述 1.1、数据链路层在计算机网络体系中的位置 1.2、对…

手机照片删除了怎么恢复?教你3个招!

我们使用手机的时间在变长&#xff0c;手机里储存的数据也会越来越多&#xff0c;所以一旦手机卡顿&#xff0c;我们往往会选择清理垃圾的方式来释放内存。如果不小心把照片清理了怎么办呢&#xff1f;手机照片删除了怎么恢复呢&#xff1f;别着急&#xff0c;这个问题小编会&a…

泰坦尼克号乘客生存预测 中

背景描述 泰坦尼克号轮船的沉没是历史上最为人熟知的海难事件之一。1912年4月15日&#xff0c;在她的处女航中&#xff0c;泰坦尼克号在与冰山相撞后沉没&#xff0c;在船上的 2224 名乘客和机组人员中&#xff0c;共造成 1502 人死亡。这场耸人听闻的悲剧震惊了国际社会&…