Redis的内存回收与内存淘汰策略

对于redis这样的内存型数据库而言,如何删除已过期的数据以及如何在内存满时回收内存是一项很重要的工作。

常见的redis内存回收的工作主要分为两个方面:

  1. 清理过期的key
  2. 在内存不足时回收到足够的内存用以存储新的key

清理过期的key

我们很少在redis中使用不带时间戳的key,因为那意味着这个key在不久之后有可能会成为死key,不为人所知,但是又占用了存储空间,这样又引出了另一个问题,不能由redis直接的去扫描过期的key并删除,这样在key的数量达到十万乃至百万级的时候,redis就会因为频繁的扫描key而陷入高CPU的不可用状态。

为了清除过期的key,redis设置特殊的策略:

  1. 惰性删除:如其名称,这种策略不会主动去删除过期的key,而是在客户端试图访问过期的key的时候进行删除
  2. 定期清除:redis有一个定时的策略会不断的抽取部分key来检查是否过期,过期的key会被清除

惰性删除

惰性删除是一个降低CPU的有效方法,将触发点放到key被访问的时候,当key被访问时,会触发一个特殊的方法expireIfNeeded,这个方法的主要内容如下所示,它的作用在于如果key过期,那么会在本次访问之后被删除。

int expireIfNeeded(redisDb *db, robj *key) {// 判断 key 是否过期if (!keyIsExpired(db,key)) return 0;....// 如果 server.lazyfree_lazy_expire 为 1 表示异步删除,反之同步删除;return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) :dbSyncDelete(db,key);
}

大致流程如下:

  1. 客户端访问带时间戳的key
  2. 服务端检查key是否已经过期,如果已经过期,那么执行删除操作,是否是同步删除取决于参数lazyfree_lazy_expire。
  3. 返回客户端null
  4. 如果没有过期,返回客户端正常的value值。

 

定期清除

我们不能期望过期的key总是能够被访问到的,我们需要主动清理key的手段。但为了减少清除带来的损耗,我们不能直接扫描所有的key,redis选择随机抽取一部分key,检查是否已经过期,如果已经过期了,那么就删除掉。

redis有两种模式用于调用特定方法清理过期的key:

  1. 定期清理:也就是slow模式,我们将主要介绍的模式,它会定期清理过期的key,同时为了减少影响会限制每次执行的总的时间,我们可以通过配置hz参数来提高扫描频率,默认情况下值为10,即每100ms扫描一次。
  2. 快速清理模式:fast模式通过方法beforeSleep执行清理方法

快速清理模式

Fast模式通过于beforeSleep方法执行,当满足以下条件之一时,将通过Fast模式清理内存中的过期key,降低内存压力

  1. 上一次任务不是因为超时而退出,且已过期键占比近似值server.stat_expired_stale_perc小于可容忍上限config_cycle_acceptable_stale。
  2. 距离上一次FAST时间,未超过指定的时间间隔,默认是2000us。

定期清理

其中,随机删除的方法是activeExpireCycle,位于文件expire.c中,随机抽取的key的数量由变量config_keys_per_loop定义,它是ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP(它本身的值为20),通过特定公式计算得出,计算公式如下:

config_keys_per_loop = ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP +ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP/4*effort

effort(值的范围是0~9,默认为0)参数由active_expire_effort得到,它表示在抽取key的时候的力度,这个值越大意味着每次遍历时抽取的key的数量就越多,同样的,性能损耗也就越大。

大致流程如下所示:

  1. 从头遍历所有的库
  2. 对每一个库先抽取桶,再对桶中的key进行遍历,如果key已经过期,那么删除它,同时计数。
  3. 如果时间已经超过限制,直接结束循环
  4. 如果循环的key的数量已经超过了限制,那么继续抽取当前库,直至时间达到限制或者过期率降低至期望以下

这个特定值表示在当前数据库中需要抽取的key的数量,config_cycle_acceptable_stale的值的大小由公式:

config_cycle_acceptable_stale=ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE - effort

计算得到,其中ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE的值为10,在代码中写死。

再根据下面的公式:

do {//抽取key并删除过期key...
} while (sampled == 0 || (expired*100/sampled) > config_cycle_acceptable_stale);

我们不难发现,默认情况下,当实际抽取的桶的数量和被释放的key的数量的比值大于10的时候,就会认定为当前数据库的过期key的数量过多,从而触发再一次的回收。

这里的回收也并不是没有时间限制的,为了减少对用户的影响,当执行时间超过某个特定值时,会直接退出本次收集,这个时间由参数timelimit确定,这个参数的计算公式为config_cycle_slow_time_perc*1000000/server.hz/100,单位为us,其中hz由参数CONFIG_DEFAULT_HZ和配置文件共同确定,默认为10,config_cycle_slow_time_perc由下面的公式确定,

config_cycle_slow_time_perc = ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC +2*effort,

ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC参数用于确定最大的CPU占比,默认为25

即默认情况下,一次定期扫描不允许超过25ms。

内存已满时淘汰key以回收内存

我们知道,如果对redis需要的内存预估错误,那么需要写入的时候就有可能会将redis的内存打满,我们可以通过配置特定的策略来处理当redis内存满的时候应当怎么做。

可选的策略有以下几种:

  • noeviction(默认策略):对于写请求不再提供服务,直接返回错误(DEL请求和部分特殊请求除外),试图写入新数据时会产生OOM异常
  • allkeys-lru:从所有key中使用LRU算法进行淘汰
  • volatile-lru:从设置了过期时间的key中使用LRU算法进行淘汰
  • allkeys-random:从所有key中随机淘汰数据
  • volatile-random:从设置了过期时间的key中随机淘汰
  • volatile-ttl:在设置了过期时间的key中,根据key的过期时间进行淘汰,越早过期的越优先被淘汰
  • volatile-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰所有设置了过期时间的键值中,最少使用的键值;
  • allkeys-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰整个键值中最少使用的键值。

不淘汰-只产生异常

这是默认的选项,使用这种策略意味着不会对任意的key进行淘汰,但由于内存已满也无法再写入任何新的数据。

看上去会产生一定的异常,但是它有它优势,它的优势在于,不会产生任何意料之外的淘汰操作,从而避免热点key意外被淘汰,导致出现缓存击穿的现象。

淘汰key回收内存

对于需要淘汰的数据,我们可以从数据范围和筛选数据的方法方面选择需要淘汰的key进行淘汰:

数据范围讲,根据key是否带时间戳可以分为:

  1. 从设置了过期时间的key中挑选需要淘汰的key
  2. 从所有的key中挑选需要淘汰的key

筛选方法方面,根据算法的不同可以分为:

  1. 随机淘汰key
  2. 通过LRU算法进行淘汰
  3. 通过LFU算法进行淘汰

需要注意的是,不论是哪种淘汰方法,需要淘汰的key的数量都是可以配置的

随机淘汰没什么好说的,就是从数据范围中随机取出N个key淘汰掉

近似LRU

严格的LRU算法是需要将所有需要管控的数据都纳入到一个链表中,每当有访问的就移动到最前面,总是淘汰链表末尾的数据

但是传统的LRU算法也有它的问题:

  • 需要用链表管理所有的缓存数据,这会带来额外的空间开销;
  • 当有数据被访问时,需要在链表上把该数据移动到头端,如果有大量数据被访问,就会带来很多链表移动操作,会很耗时,进而会降低 Redis 缓存性能。

redis选用近似的LRU算法,放弃了链表式的结构以此来最大限度的减少算法执行过程中的性能损耗。

redis对象内部维护了一个特殊的字段,针对不同的回收策略会有不同的用处,对于LRU来讲,它是一个24位的时钟,记录对象保存到redis的时间,对于LFU算法而言,它是用来保存访问时间以及频率的字段。

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;

因为不维护字段,所以对于需要淘汰的key的选拔是通过在数据范围内,随机筛选出N(可配置)个key,选择最旧的一个数据淘汰掉,这样做的优势在于能够节省在损耗过程中的性能损耗。

对比严格LRU算法,它的优势在于:

  • 节省了保存链表的空间
  • 降低了频繁移动表节点带来的CPU的损耗

它的问题在于,近似始终是近似,在效果上必然不如严格LRU算法那么精确。

如下图所示,在选取数为10的情况下,可以看到是大多数古老的key是已经被淘汰的,同时对新的key影响也比较低,与严格LRU效果已经接近,已经足够满足我们的需求。

 

近似LFU算法也有LFU算法的固有问题,就是LFU实际上只有时间的概念,它是没有热度这个说法的,也就是说,我们在淘汰过程中很可能会淘汰掉一个热点key,或者在大批新的数据写入时,会影响对旧数据的判断。

LFU算法

我们之前提到,redis的对象中维护了一个字段,在回收算法是LFU的时候,这个字段的作用在于记录最近访问时间以及频率,这是一个24位的字段,它的前16位用于记录时间,后8位则记录频率。

增长策略

counter并不是简单的访问一次就+1,而是采用了一个0-1之间的p因子控制增长。counter最大值为255。取一个0-1之间的随机数r与p比较,当r<p时,才增加counter,这和比特币中控制产出的策略类似。p取决于当前counter值与lfu_log_factor因子,counter值与lfu_log_factor因子越大,p越小,r<p的概率也越小,counter增长的概率也就越小。

降低策略

既然与热度相关,那么自然就会有降低的策略,下降的周期也可以通过参数配置,配置的含义在于每过多久下降1,这样下次计算时可以通过时间差直接计算出当前的key下降后的热度。

下面是LFU算法的热度增长代码:

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;}

可以看到,对于任意的key,它的热度越高,计算得到的允许热度上涨的p就越小,热度上升就需要更多次的访问,同时还可以通过lfu_log_factor参数来控制增长速率,它的值越大,增长速度就越小。

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

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

相关文章

Cesium态势标绘专题-普通点(标绘+编辑)

标绘专题介绍:态势标绘专题介绍_总要学点什么的博客-CSDN博客 入口文件:Cesium态势标绘专题-入口_总要学点什么的博客-CSDN博客 辅助文件:Cesium态势标绘专题-辅助文件_总要学点什么的博客-CSDN博客 本专题没有废话,只有代码,代码中涉及到的引入文件方法,从上面三个链…

[golang gin框架] 40.Gin商城项目-微服务实战之Captcha验证码微服务

本次内容需要 gin框架基础知识, golang微服务基础知识才能更好理解 一.Captcha验证码功能引入 在前面,讲解了微服务的架构等,这里,来讲解前面商城项目的 Captcha验证码 微服务 ,captcha验证码功能在前台,后端 都要用到 ,可以把它 抽离出来 ,做成微服务功能 编辑 这个验证码功能…

《零基础入门学习Python》第063讲:论一只爬虫的自我修养11:Scrapy框架之初窥门径

上一节课我们好不容易装好了 Scrapy&#xff0c;今天我们就来学习如何用好它&#xff0c;有些同学可能会有些疑惑&#xff0c;既然我们懂得了Python编写爬虫的技巧&#xff0c;那要这个所谓的爬虫框架又有什么用呢&#xff1f;其实啊&#xff0c;你懂得Python写爬虫的代码&…

20 QTreeWidget控件

代码&#xff1a; //treeWidget树控件//1&#xff1a;设置头部标签 QStringList()匿名对象创建ui->treeWidget->setHeaderLabels(QStringList()<<"英雄"<<"英雄介绍");//2&#xff1a;设置itemQTreeWidgetItem * liItem new QTreeWidg…

Linux 系统中异常与中断

文章目录 异常与中断的关系中断的处理流程异常向量表Linux 系统对中断的处理ARM 处理器程序运行的过程程序被中断时&#xff0c;怎么保存现场Linux 系统对中断处理的演进Linux 对中断的扩展&#xff1a;硬件中断、软件中断硬件中断软件中断 中断处理原则&#xff1a;耗时中断的…

【面试题】万字总结MYSQL面试题

Yan-英杰的主页 悟已往之不谏 知来者之可追 C程序员&#xff0c;2024届电子信息研究生 目录 1、三大范式 2、DML 语句和 DDL 语句区别 3、主键和外键的区别 4、drop、delete、truncate 区别 5、基础架构 6、MyISAM 和 InnoDB 有什么区别&#xff1f; 7、推荐自增id作为…

【C++】STL---list基本用法介绍

个人主页&#xff1a;平行线也会相交&#x1f4aa; 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 平行线也会相交 原创 收录于专栏【C之路】&#x1f48c; 本专栏旨在记录C的学习路线&#xff0c;望对大家有所帮助&#x1f647;‍ 希望我们一起努力、成长&…

Kotlin基础(七):数据类和封闭类

前言 本文主要讲解kotlin数据类&#xff08;DataClass&#xff09;和封闭类&#xff08;SealedClasses&#xff09;&#xff0c;包括使用数据类&#xff0c;对象复制&#xff0c;数据类成员的解构&#xff0c;使用封闭类&#xff0c;以及数据类和封闭类在Android开发中的应用。…

el-select和el-checkBox实现下拉菜单全选功能

el-select 和 el-checkbox 实现下拉菜单全选功能 示例代码&#xff1a; <el-selectpopper-class"select-container"v-model"ids"placeholder"请选择目标":multiple-limit"20"multiplefilterablecollapse-tagsclass"wd400&qu…

uniapp 小程序 查看评价

查看评价效果图&#xff1a; 评分组件在上一篇文章&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01; <template><view class"view-comments"><view class"evaluate-box"><view class"ti…

使用Gin框架搭配WebSocket完成实时聊天

文章目录 前言实时聊天聊天功能测试发送信息 前言 在写项目的时候&#xff0c;需要完成实时聊天的功能&#xff0c;于是简单的学习下WebSocket&#xff0c;想知道WebSocket是什么的小伙伴可以去网上别的地方学习一下。 要实现实时聊天&#xff0c;网上的大部分内容都是Spring…

Rust操作MySQL

查询 本部分是对 「Rust入门系列」Rust 中使用 MySQL[1]的学习与记录 经常使用的时间处理库&#xff1a; chrono 流式查询使用&#xff1a; query_iter 输出到Vec使用&#xff1a; query 映射到结构体使用&#xff1a; query_map 获取单条数据使用&#xff1a; query_first 命名…

小程序体验版上线注意事项

1.接口域名必须是https&#xff0c;有ssh证书。不能用ip地址。 2.需要在微信公众平台进行配置 微信公众平台->开发-> 开发管理->开发设置 对服务器域名和业务域名进行配置对业务域名进行配置时&#xff0c;需要下载校验文件&#xff0c;放在域名根目录下

【Node.js 安装】Node.js安装与使用教程

Node.js 安装 Node.js 是什么那什么是运行时 如何安装 Node.jsNode 使用教程 Node.js 是什么 先说结论&#xff0c;Node.js 它是一套 JavaScript 运行环境&#xff0c;用来支持 JavaScript 代码的执行 JavaScript 诞生于 1995 年&#xff0c;几乎是和互联网同时出现&#xf…

十分钟掌握 Vim 编辑器核心功能

十分钟掌握 Vim 编辑器核心功能 文章目录 十分钟掌握 Vim 编辑器核心功能&#x1f468;‍&#x1f3eb;内容一&#xff1a;前言【Vim是什么】&#x1f468;‍&#x1f52c;内容二&#xff1a;Vim 常用模式&#x1f468;‍&#x1f680;内容三&#xff1a;基本操作&#x1f468;…

【C语言】表达式求值相关问题汇总—>隐式类型转换(整型提升)、算数转换与操作符优先级汇总(收藏查阅)

&#x1f440;樊梓慕&#xff1a;个人主页 &#x1f3a5;个人专栏&#xff1a;《C语言》《数据结构》《蓝桥杯试题》 &#x1f31d;每一个不曾起舞的日子&#xff0c;都是对生命的辜负。 目录 前言&#xff1a; 一、隐式类型转换 &#xff08;一&#xff09;整型提升的意义…

Dubbogo 详解

Dubbogo 详解 简介 dubbo功能很强大的微服务开发框架&#xff0c;支持多种通信协议&#xff0c;并具有流量治理的功能。 dubbo在有了大转变&#xff0c;拥抱了云原生&#xff0c;从哪些方面可以体现呢&#xff1f; 推出了自己的Trip协议修复了服务发现的级别&#xff0c;之…

华为云CodeArts产品体验的心得体会及想法

文章目录 前言CodeArts 的产品优势一站式软件开发生产线研发安全Built-In华为多年研发实践能力及规范外溢高质高效敏捷交付 功能特性说明体验感受问题描述完结 前言 华为云作为一家全球领先的云计算服务提供商&#xff0c;致力于为企业和个人用户提供高效、安全、可靠的云服务。…

算法与数据结构(二十一)前缀和数组差分数组

前缀和技巧适用于快速、频繁地计算一个索引区间内的元素之和。 1. 一维数组中的前缀和 先看一道例题&#xff0c;力扣第 303 题「区域和检索 - 数组不可变」&#xff0c;让你计算数组区间内元素的和&#xff0c;这是一道标准的前缀和问题&#xff1a; 题目要求你实现这样一个…

【C++初阶】---C++入门篇

文章目录 前言&#x1f31f;一、C历史介绍&#x1f31f;二、命名空间&#x1f30f;2.1.C与C对比&#x1f30f;2.2.命名空间的引入&#x1f30f;2.3.命名空间定义&#x1f30f;2.4.命名空间的使用&#x1f30f;2.5.对上述C与C对比中的第二个不同点的解释&#xff1a; &#x1f3…