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验证码功能在前台,后端 都要用到 ,可以把它 抽离出来 ,做成微服务功能 编辑 这个验证码功能…

【vue3 自定义组件中使用v-model实现双向绑定】

文章目录 前言简单封装Input组件v-mode参数多个v-model绑定 前言 比如我们有自定义的Form组件、Input组件。 如果Form组件想拿到Input组件中input框输入的内容&#xff0c;我们可以让Form这个父组件给Input子组件传值props:value&#xff08;不能直接修改子组件的props&#…

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

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

【Spring MVC】小文件上传的多种方法

文章目录 方法参数单文件上传1. MultipartFile 的 transferTo(File dest)2. MultipartFile 的 transferTo(Path dest)3. MultipartFile Files.write(Path path, byte[] bytes, OpenOption... options)4. MultipartFile Files.copy(InputStream in, Path target, CopyOption..…

20 QTreeWidget控件

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

Kubernetes(K8s)从入门到精通系列之二:入门案例启动MySQL服务和Tomcat应用

Kubernetes K8s 从入门到精通系列之二:入门案例启动MySQL服务和Tomcat应用 一、实际应用案例二、部署K8s集群三、创建Mysql的Deployment,启动MySQL服务四、创建MySQL的Service五、创建tomcat的Deployment,启动Tomcat应用六、通过浏览器访问网页一、实际应用案例 运行在Tomca…

MySQL IF()函数:在查询中灵活应用条件逻辑

前言&#xff1a; 在数据库查询中&#xff0c;我们经常需要根据条件逻辑来选择返回不同的结果。MySQL提供了强大的IF()函数&#xff0c;使得在查询语句中应用条件逻辑变得非常简单和灵活。本篇文章将深入探讨MySQL的IF()函数&#xff0c;并展示如何在查询中利用它来进行条件判断…

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作为…

Jvm 之 Stop The World 机制

文章目录 一、STW简介二、为什么需要STW三、STW机制触发实际1. 垃圾回收&#xff08;GC&#xff09;&#xff1a;2. 类加载和卸载&#xff1a;3. JIT编译&#xff1a; 四、STW带来的问题1. 停顿时间延长&#xff1a;2. 性能下降&#xff1a;3. 延迟累积&#xff1a;4. 系统资源…

深入浅出Pytorch函数——torch.sort

分类目录&#xff1a;《深入浅出Pytorch函数》总目录 按照值沿给定维度对输入张量的元素进行排序。如果未给定dim&#xff0c;则选择输入的最后一个维度。若descending被指定为True&#xff0c;则元素按值降序排列&#xff0c;否则为升序。如果stable为True&#xff0c;则排序例…

【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…

ModuleNotFoundError: No module named ‘sklearn‘ 应该如何解决

该错误提示表明你的环境中缺少名为 sklearn 的模块&#xff0c;也就是 scikit-learn 包 要解决这个问题&#xff0c;你可以尝试以下几个步骤&#xff1a; 确认是否安装了 scikit-learn&#xff1a;请确保你已经在你的环境中安装了 scikit-learn。你可以使用以下命令来安装&am…

Debezium系列之:使用 Strimzi 将 Kafka 和 Debezium 迁移到 Kubernetes

Debezium系列之:使用 Strimzi 将 Kafka 和 Debezium 迁移到 Kubernetes 一、Kubernetes二、认识Strimzi三、安装 Strimzi四、创建Kafka集群五、创建Kafka Topic六、部署 Debezium Kafka Connect七、总结在本文中,将探讨在生产中实现debezium与K8s的结合: 在 Kubernetes 集群…

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…

MacBookPro安装Win10,Wifi不能用了,触控板不能用了

由于工作需要&#xff0c;得在MacBookPro 2016上安装Windows系统&#xff1b;但是偶尔也需要用Mac系统&#xff0c;只好安装双系统。 苹果提供的双系统安装方案还是相当好用的&#xff1a;“启动转换助理”。去年安装完Win10后&#xff0c;触控板本身就支持双点相当于鼠标右键…