Redis 的 LRU 与 LFU 算法实现

news2024/11/18 3:27:21

一、前言

原文地址
Redis是一款基于内存的高性能NoSQL数据库,数据都缓存在内存里, 这使得Redis可以每秒轻松地处理数万的读写请求。
相对于磁盘的容量,内存的空间一般都是有限的,为了避免Redis耗尽宿主机的内存空间,Redis内部实现了一套复杂的缓存淘汰策略来管控内存使用量。
Redis 4.0版本开始就提供了8种内存淘汰策略,其中4种都是基于LRU或LFU算法实现的,本文就这两种算法的Redis实现进行了详细的介绍,并阐述其优劣特性。

二、Redis的LRU实现

在介绍Redis LRU算法实现之前,我们先简单介绍一下原生的LRU算法。

2.1 LRU算法原理

LRU(The Least Recently Used)是最经典的一款缓存淘汰算法,其原理是 :如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很低,当数据所占据的空间达到一定阈值时,这个最少被访问的数据将被淘汰掉。
如今,LRU算法广泛应用在诸多系统内,例如Linux内核页表交换,MySQL Buffer Pool缓存页替换,以及Redis数据淘汰策略。
以下是一个LRU算法示意图:

  1. 向一个缓存空间依次插入三个数据A/B/C,填满了缓存空间;
  2. 读取数据A一次,按照访问时间排序,数据A被移动到缓存头部;
  3. 插入数据D的时候,由于缓存空间已满,触发了LRU的淘汰策略,数据B被移出,缓存空间只保留了D/A/C。
    在这里插入图片描述
    一般而言,LRU算法的数据结构不会如示意图那样,仅使用简单的队列或链表去缓存数据,而是会采用Hash表 + 双向链表的结构,利用Hash表确保数据查找的时间复杂度是O(1),双向链表又可以使数据插入/删除等操作也是O(1)。
    在这里插入图片描述
    如果你很熟悉Redis的数据类型,你会发现这个LRU的数据结构与ZSET类型OBJ_ENCODING_SKIPLIST编码结构相似,只是LRU数据排序方式更简单一些。

2.2 Redis LRU算法实现

按照官方文档的介绍,Redis所实现的是一种近似的LRU算法,每次随机选取一批数据进行LRU淘汰,而不是针对所有的数据,通过牺牲部分准确率来提高LRU算法的执行效率。
Redis内部只使用Hash表缓存了数据,并没有创建一个专门针对LRU算法的双向链表,之所以这样处理也是因为以下几个原因:
● 筛选规则,Redis是随机抽取一批数据去按照淘汰策略排序,不再需要对所有数据排序;
● 性能问题,每次数据访问都可能涉及数据移位,性能会有少许损失;
● 内存问题,Redis对内存的使用一向很“抠门”,数据结构都很精简,尽量不使用复杂的数据结构管理数据;
● 策略配置,如果线上Redis实例动态修改淘汰策略会触发全部数据的结构性改变,这个Redis系统无法承受的。
redisObject是Redis核心的底层数据结构,成员变量lru字段用于记录了此key最近一次被访问的LRU时钟(server.lruclock),每次Key被访问或修改都会引起lru字段的更新。

#define LRU_BITS 24
 
typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). */
    int refcount;
    void *ptr;
} robj;

默认的LRU时钟单位是秒,可以修改LRU_CLOCK_RESOLUTION宏来改变单位,LRU时钟更新的频率也和server.hz参数有关。

unsigned int LRU_CLOCK(void) {
    unsigned int lruclock;
    if (1000/server.hz <= LRU_CLOCK_RESOLUTION) {
        atomicGet(server.lruclock,lruclock);
    } else {
        lruclock = getLRUClock();
    }
    return lruclock;
}

由于lru字段仅占用了24bit的空间,按秒为单位也只能存储194天,所以可能会出现一个意想不到的结果,即间隔194天访问Key后标记的时间戳一样,Redis LRU淘汰策略局部失效。

2.3 LRU算法缺陷

LRU算法仅关注数据的访问时间或访问顺序,忽略了访问次数的价值,在淘汰数据过程中可能会淘汰掉热点数据。
如下图所示,时间轴自左向右,数据A/B/C在同一段时间内被分别访问的数次。数据C是最近一次访问的数据,按照LRU算法排列数据的热度是C>B>A,而数据的真实热度是B>A>C。
在这里插入图片描述

这个是LRU算法的原理性问题,自然也会在Redis 近似LRU算法中呈现,为了解决这个问题衍生出来LFU算法。

三、Redis的LFU实现

3.1 LFU算法原理

LFU(Least frequently used)即最不频繁访问,其原理是:如果一个数据在近期被高频率地访问,那么在将来它被再访问的概率也会很高,而访问频率较低的数据将来很大概率不会再使用。

很多人看到上面的描述,会认为LFU算法主要是比较数据的访问次数,毕竟访问次数多了自然访问频率就高啊。实际上,访问频率不能等同于访问次数,抛开访问时间谈访问次数就是在“耍流氓”。
在这里插入图片描述
在这段时间片内数据A被访问了5次,数据B与C各被访问了4次,如果按照访问次数判断数据热度值,必然是A>B=C;如果考虑到时效性,距离当前时间越近的访问越有价值,那么数据热度值就应该是C>B>A。因此,LFU算法一般都会有一个时间衰减函数参与热度值的计算,兼顾了访问时间的影响。

LFU算法实现的数据结构与LRU一样,也采用Hash表 + 双向链表的结构,数据在双向链表内按照热度值排序。如果某个数据被访问,更新热度值之重新插入到链表合适的位置,这个比LRU算法处理的流程复杂一些。

3.2 Redis LFU算法实现

Redis 4.0版本开始增加了LFU缓存淘汰策略,也采用数据随机筛选规则,然后依据数据的热度值排序,淘汰掉热度值较低的数据。

3.2.1 LFU算法代码实现

LFU算法的实现没有使用额外的数据结构,复用了redisObject数据结构的lru字段,把这24bit空间拆分成两部分去使用。

由于记录时间戳在空间被压缩到16bit,所以LFU改成以分钟为单位,大概45.5天会出现数值折返,比LRU时钟周期还短。
低位的8bit用来记录热度值(counter),8bit空间最大值为255,无法记录数据在访问总次数。
在这里插入图片描述

LFU热度值(counter)的算法实现:

#define LFU_INIT_VAL 5
 
/* Logarithmically increment a counter. The greater is the current counter value
 * the less likely is that it gets really implemented. Saturate it at 255. */
uint8_t LFULogIncr(uint8_t counter) {
  if (counter == 255) return 255;
  double r = (double)rand()/RAND_MAX;
  double baseval = counter - LFU_INIT_VAL;
  if (baseval < 0) baseval = 0;
  double p = 1.0/(baseval*server.lfu_log_factor+1);
  if (r < p) counter++;
  return counter;
}

● counter 小于或等于 LFU_INIT_VAL 时候,数据一旦被访问命中, counter接近100%概率递增1;
● counter 大于 LFU_INIT_VAL 时候,需要先计算两者差值,然后作为分母的一部分参与递增概率的计算;
● 随着counter 数值的增大,递增的概率逐步衰减,可能数次的访问都不能使其数值加1;
● 当counter 数值达到255,就不再进行数值递增的计算过程。
LFU counter的计算也并非“一尘不变”,为了适配各种业务数据的特性,Redis在LFU算法实现过程中引入了两个可调参数:
在这里插入图片描述

热度值counter的时间衰减函数:
 
unsigned long LFUDecrAndReturn(robj *o) {
    unsigned long ldt = o->lru >> 8;
    unsigned long counter = o->lru & 255;
    unsigned long num_periods = server.lfu_decay_time ? LFUTimeElapsed(ldt) / server.lfu_decay_time : 0;
    if (num_periods)
        counter = (num_periods > counter) ? 0 : counter - num_periods;
    return counter;
}

阅读完以上的内容,是否感觉似曾相似?实际上LFU counter计算过程就是对访问次数进行了数值归一化,将数据访问次数映射成热度值(counter),数值的范围也从 [0,+∞) 映射到另一个维度的 [0,255] 。

3.3.2 LFU Counter分析

仅从代码层面分析研究Redis LFU算法实现会比较抽象且枯燥,无法直观的呈现counter递增概率的算法效果,以及counter数值与访问次数的关系。
lfu_log_factor为默认值10的场景下,利用Python实现Redis LFU算法流程,绘制出LFU counter递增概率曲线图:
在这里插入图片描述
可以清晰的观察到,当LFU counter数值超过LFU_INIT_VAL之后,曲线出现了垂直下降,递增概率陡降到0.2%左右,随后在底部形成一个较为缓慢的衰减曲线,直至counter数值达到255则递增概率归于0,贴合3.3.1章节分析的理论。

保持Redis系统配置默认值的情况下,对同一个数据持续的访问,并采集此数据的LFU counter数值,绘制出LFU counter数值曲线图:
在这里插入图片描述
随着访问次数的不断增加,LFU counter数值曲线呈现出爬坡式的递增,形态趋近于根号曲线,由此推测出以下观点:
● 在访问次数相同的情况下,counter数值不是固定的,大概率在一个范围内波动;
● 在同一个时间段内,数据之间访问次数相差上千次,才可以通过counter数值区分出哪些数据更热,而“温”数据之间可能很难区分热度。

四、总结

通过对Redis LRU与LFU算法实现的介绍,我们可以大体了解两种算法策略的优缺点,在Redis运维过程中,可以依据业务数据的特性去选择相应的算法。

如果业务数据的访问较为均匀,OPS或CPU利用率一般不会出现周期性的陡升或陡降,数据没有体现出相对的“冷热”特性,即建议采用LRU算法,可以满足一般的运维需求。

相反,业务具备很强时效性,在活动推广或大促期间,业务某些数据会突然成为热点数据,监控上呈现出OPS或CPU利用率的大幅波动,为了能抓取热点数据便于后期的分析或优化,建议一定要配置成LFU算法。

在Used_memory接近Maxmemory的情况下,Redis一直都采用随机的方式筛选数据,且筛选的个数极其有限,所以,LFU算法无法展现出较大的优势,也可能会淘汰掉比较热的数据。

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

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

相关文章

【高通WLAN】WLAN bdf如何配置支持UMC设备和TSMC设备

除了用于台积电(TSMC)设备的现有bdf文件外,软件升级中还包括新的WLAN bdf文件,以支持UMC设备。 bdf文件导入的路径是 wlan_proc\wlan\halphy_tools\host\bdfUtil\qca61x0\bdf ■ TSMC(现有):bdwlan.xxx至bdwlan.bin ■ UMC(新):bdwlanu.xxx至bdwlanu.bin 以上配置…

可以带着游泳的耳机有哪些,推荐几款可以在水下使用的游泳耳机

在夏天里游泳是一件相当舒适的运动项目&#xff0c;比起汗流浃背的健身房&#xff0c;游泳池已经成为了更多人所倾向的地方&#xff0c;因为它不仅可以起到塑性的效果&#xff0c;而且还能够让自己时长保持在舒爽的状态&#xff0c;这时就会有朋友在问&#xff0c;游泳的时候节…

IDEA介绍

集成开发环境&#xff08;IDE&#xff09;简介 集成开发环境&#xff08;IDE&#xff0c;Integrated Development Environment&#xff09;是为程序开发提供便利的应用程序。通常包括代码编辑器、编译器、调试器和图形用户界面等工具。它们集成了代码编写、分析、编译、调试等…

时间序列预测 | Matlab鲸鱼算法(WOA)优化极限梯度提升树XGBoost时间序列预测,WOA-XGBoost时间序列预测模型,单列数据输入模型

文章目录 效果一览文章概述部分源码参考资料效果一览 文章概述 鲸鱼算法(WOA)优化极限梯度提升树XGBoost时间序列预测,WOA-XGBoost时间序列预测模型,单列数据输入模型 评价指标包括:MAE、RMSE和R2等,代码质量极高,方便学习和替换数据。要求2018版本及以上。 部分源码 %% …

基于单片机停车场刷卡收费的设计与实现

功能介绍 以51单片机作为主控系统&#xff1b;以51单片机作为主控系统&#xff1b;通过液晶显示当前时间&#xff0c;车位、剩余车位、时间等信息&#xff1b;进电机正反转表示开关门&#xff1b;按键可以设置当前时间/单价/分钟&#xff1b;RC522射频卡识别&#xff0c;当刷卡…

Qt Core学习日记——第二天QMetaClassInfo

QMetaClassInfo是QMetaObject中用于存放类信息的数据结构 QMetaClassInfo定义&#xff1a; class Q_CORE_EXPORT QMetaClassInfo { public: Q_DECL_CONSTEXPR inline QMetaClassInfo() : mobj(nullptr), handle(0) {} const char *name() const; const char *value() co…

使用Jetpack Compose中的Pager构建滑动页面

Jetpack Compose是Google为Android开发者提供的一种现代化的UI工具包&#xff0c;它采用声明式UI范式&#xff0c;使得开发者能够更加简洁、直观地构建用户界面。在这篇博客中&#xff0c;我们将探讨如何使用Jetpack Compose中的Pager构建滑动页面。 什么是Pager&#xff1f; …

如何查看Linux是否开启了数据包转发功能

如果Linux主机有多块网卡&#xff0c;如果不开启数据包转发功能&#xff0c;则这些网卡之间是无法互通的。 如何开启数据包转发功能&#xff1a; echo 1 > /proc/sys/net/ipv4/ip_forward sysctl -w net.ipv4.ip_forward1 如何查看是否开启了数据包转发功能&#xff1a; …

Some Strategies for Reducing Write Amplification in LSM-tree

写放大原理 (6 封私信) 如何理解SSD的写放大? - 知乎 (zhihu.com) 日志结构化合并树(LSM 树) LSM 树: 与应用就地更新的传统

直播预约|漫漫用户增长之路,如何快人一手

【导读】 如何实现用户增长及提升用户活跃度是各大开发者关注的重点之一&#xff0c;日常运营过程中&#xff0c;消息推送作为一个触达用户的有效手段&#xff0c;通过恰当的时机和智能的投放模式&#xff0c;可以有效提升消息的曝光度并吸引用户点击&#xff0c;从而实现用户…

​DMBOK知识梳理for CDGA/CDGP——第十章 参考数据与主数据(附常考知识点)

第十章 参考数据与主数据 第十章在CDGA分值占比不高&#xff0c;CDGP分值占比较高&#xff0c;主要考点包括&#xff1a;定义、目标、原则、参考数据及主数据管理好处、异同点、哪些属于主数据、活动、工具、度量指标等基本概念。因此本章建议充分理解参考数据及主数据的基础概…

开放式耳机品牌推荐,热门开放式耳机大盘点!

随着生活的提升&#xff0c;越来越多小伙伴开始使用开放式耳机了&#xff0c;因为开放式耳机不入耳不伤耳的设计&#xff0c;佩戴稳固又舒适&#xff0c;还具有良好的音质和舒适的佩戴体验。下面我来给大家安利几款很不错的开放式耳机&#xff0c;来看看有哪些吧。 一、NANK南…

违反广告法被罚3万元?苹果北京公司又因买卖合同纠纷成被执行人

根据中国执行信息公开网等消息&#xff0c;苹果电子产品商贸&#xff08;北京&#xff09;有限公司最近在买卖合同纠纷方面被执行&#xff0c;执行金额为6442元&#xff0c;案件由上海市浦东新区人民法院审理&#xff0c;目前尚未获取到详细纠纷信息。 近日&#xff0c;苹果公司…

一个月学通Python(十二):Python发送电子邮件及图像办公文档处理

专栏介绍 结合自身经验和内部资料总结的Python教程&#xff0c;每天3章&#xff0c;1个月就能全方位的完成Python的学习并进行实战开发。加油吧&#xff01;卷起来&#xff01; 全部文章请访问专栏&#xff1a;《Python全栈教程&#xff08;0基础》 文章目录 专栏介绍网络应用…

在 Jetpack Compose 中使用 BottomAppBar

简介 Jetpack Compose 是一个现代化的、声明式的 UI 工具包&#xff0c;它使我们能够更方便地构建 Android 的用户界面。本篇文章将介绍如何在 Jetpack Compose 中使用 BottomAppBar 来创建底部应用栏。 什么是 BottomAppBar? BottomAppBar 是一个在屏幕底部的应用栏&#x…

学习笔记——vscode界面设置界面缩放级别

使用vscode时&#xff0c;不知道按了什么快捷键&#xff0c;vscode窗口缩放了。 调整方法&#xff1a;设置 > 窗口(window) > Zoom Level

第一百零一天学习记录:C++核心:类和对象Ⅵ(五星重要)继承上

继承 继承是面向对象三大特性之一 继承的基本语法 普通写法&#xff1a; #include <iostream> using namespace std;//普通实现页面//Java页面 class Java { public:void header(){cout << "首页、公开课、登录、注册……&#xff08;公共头部&#xff09…

电子森林STEP-MXO2_1 入门部分全部实验

前言 本部分实验基于电子森林小脚丫开发板的数电入门教程实验。实验链接&#xff1a;step-mxo2入门教程 电子森林] (eetree.cn) 其中代码是博主学习后根据自己思路自己敲的&#xff0c;并非直接复制&#xff0c;且仅供学习交流使用&#xff0c;侵删。 lattice 环境配置在此不…

【解压缩技巧】2种方法合并ZIP分卷压缩文件

文件太大的时候&#xff0c;很多人都会选择“分卷压缩”来压缩ZIP文件&#xff0c;那分卷后的ZIP文件要怎么合并回去呢&#xff1f;今天小编就来分享2个合并方法&#xff0c;下面一起看看吧。 方法一&#xff1a; 使用7-Zip压缩软件的自带“合并文件”功能。 首先打开7-Zip&a…

Mac下pom.xml文件中找不到env.JAVA_HOME

Mac 11.7.6 这个是解决后的样子&#xff0c;解决前是env.JAVA_HOME找不到 上图中的${env.JAVA_HOME}中的env是用来获取系统环境变量&#xff0c;但是在mac10以上的版本,即使我们在bash_profile文件中配置了JAVA_HOME&#xff0c;这里也不能直接使用env将JAVA_HOME点出来&#…