首页 > 数据库 >[Redis]过期删除和内存淘汰

[Redis]过期删除和内存淘汰

时间:2024-07-22 11:52:40浏览次数:10  
标签:删除 过期 Redis 链表 内存 key

过期删除

Redis 提供了四个命令来设置过期时间(生存时间):

  • EXPIRE :表示将键 key 的生存时间设置为 ttl 秒;
  • PEXPIRE :表示将键 key 的生存时间设置为 ttl 毫秒;
  • EXPIREAT :表示将键 key 的生存时间设置为 timestamp 所指定的秒数时间戳;
  • PEXPIREAT :表示将键 key 的生存时间设置为 timestamp 所指定的毫秒数时间戳。

在Redis内部实现中,前面三个设置过期时间的命令最后都会转换成最后一个PEXPIREAT 命令来完成。

PERSIST :表示将 key 的过期时间移除。

查看键的剩余过期时间
TTL :以秒的单位返回键 key 的剩余生存时间;
PTTL :以毫秒的单位返回键 key 的剩余生存时间。

在 Redis 内部,每当我们设置一个键的过期时间时,Redis 就会将该键带上过期时间存放到一个过期字典中。
当我们查询一个键时,Redis 首先检查该键是否存在过期字典中,如果存在,那就获取其过期时间,然后将过期时间和当前系统时间进行比对,比系统时间大,那就没有过期;反之判定该键过期。

定时删除

在设置某个 key 的过期时间同时,我们创建一个定时器,让定时器在该过期时间到来时,立即执行对其进行删除的操作。

  • 优点:定时删除对内存是最友好的,能够保存内存的 key 一旦过期就能立即从内存中删除;
  • 缺点:对 CPU 最不友好,在过期键比较多的时候,删除过期键会占用一部分 CPU 时间,对服务器的响应时间和吞吐量造成影响。

惰性删除

设置该 key 过期时间后,我们不去管它,当需要该 key 时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该 key。
优点:对 CPU 友好,我们只会在使用该键时才会进行过期检查,对于很多用不到的 key 不用浪费时间进行过期检查;
缺点:对内存不友好,如果一个键已经过期,但是一直没有使用,那么该键就会一直存在内存中,如果数据库中有很多这种使用不到的过期键,这些键便永远不会被删除,内存永远不会释放,从而造成内存泄漏。

定期删除

每隔一段时间,我们就对一些 key 进行检查,删除里面过期的 key。
优点:可以通过限制删除操作执行的时长和频率来减少删除操作对 CPU 的影响。另外定期删除,也能有效释放过期键占用的内存。
缺点:难以确定删除操作执行的时长和频率。如果执行的太频繁,定期删除策略变得和定时删除策略一样,对CPU不友好。如果执行的太少,那又和惰性删除一样了,过期键占用的内存不会及时得到释放。另外最重要的是,在获取某个键时,如果某个键的过期时间已经到了,但是还没执行定期删除,那么就会返回这个键的值,这是业务不能忍受的错误。

Redis 使用的过期删除策略

通过前面讨论的三种过期删除策略,可以发现单一使用某一策略都不能满足实际需求,所以实际的应用都是组合使用的,而 Redis 使用的过期删除策略就是:惰性删除和定期删除两种策略的组合。

  • Redis 的惰性删除策略由 db.c/expireIfNeeded 函数实现,所有键读写命令执行之前都会调用 expireIfNeeded 函数对其进行检查,如果过期,则删除该键,然后执行键不存在的操作;未过期则不作操作,继续执行原有的命令。
  • Redis 的定期删除策略由 redis.c/activeExpireCycle 函数实现,函数以一定的频率运行,每次运行时,都从一定数量的数据库中取出一定数量的随机键进行检查,并删除其中的过期键
    需要注意的是: Redis 的定期删除策略并不是一次运行就检查所有的库、所有的键,而是随机检查一定数量的键
    定期删除函数的运行频率,在 Redis2.6 版本中,规定每秒运行 10 次,大概 100ms 运行一次。在 Redis2.8 版本后,可以通过修改配置文件 redis.conf 的 hz 选项来调整这个次数:
# The range is tetween 1 and 500, however a value over 100 is usually not
# a good idea. Most users should use the default of 10 and raise this up to
# 100 only in environments where very low latency is requiried.
hz 10

从这个参数的上面注释可以看出,建议不要将这个值设置超过100,一般使用默认的10,只有当在需要非常低延迟的场景才设置为100。

Redis 过期删除策略的问题

虽然 Redis 采用了惰性删除和定期删除两种策略,但对于一些永远使用不到的键,并且经过多次定期删除也没有被选定到并删除,那么这些键就会一直驻留在内存中。
所以,这时候就需要使用 Redis 的内存淘汰策略来解决了。

内存淘汰

设置 Redis 最大内存
在配置文件 redis.conf 中,可以通过参数 maxmemory 来设定最大内存:

# In short... if you have slaves attached it is suggested that you set a lower
# limit for maxmemory so that there is some free RAM on the system for slave
# output buffers (but this is not needed if the policy is 'noeviction').
#
maxmemory <bytes>
# MAXMEMORY POLICY: how Redis will select what to remove when maxmemory
# is reached. You can select among five behaviors:
#
# volatile-lru -> remove the key with an expire set using an LRU algorithm
# allkeys-lru -> remove any key according to the LRU algorithm
# volatile-random -> remove a random key with an expire set
# allkeys-random -> remove a random key, any key
# volatile-ttl -> remove the key with the nearest expire time (minor TTL)
# noeviction -> don't expire at all, just return an error on write operations

不设定该参数默认是无限制的,但是通常会设定其为物理内存的四分之三。

内存淘汰策略

当使用的内存大于 maxmemory 时,就会触发 Redis 主动淘汰内存方式

volatile-lru:针对设置了过期时间的key,使用 lru 算法进行淘汰;
allkeys-lru:针对所有key使用 lru 算法进行淘汰(推荐,使用的较多);
volatile-lfu:针对设置了过期时间的 key,使用 lfu 算法进行淘汰;
allkeys-lfu:针对所有key使用 lfu 算法进行淘汰;
volatile-random:针对设置了过期时间的 key ,使用随机淘汰的方式进行淘汰;
allkeys-random:针对所有的key使用随机淘汰机制进行淘汰;
volatile-ttl:删除最近即将过期的一个键(minor TTL);
noeviction(默认):不删除键,值返回错误(不建议使用)。

volatile 表示有过期的时间的 key;
allkeys 表示所有的 key;
lru(Least Recently Used)表示最近最少使用(根据时间,最不常用的淘汰);
lfu(Least Frequently Used)表示使用次数最少(根据计数器,用的次数最少的 key 淘汰);
ttl(time to live) 表示即将过期;

内存淘汰算法的具体工作原理是:
客户端执行一条新命令,导致数据库需要增加数据(比如 set key value);
Redis 会检查内存使用,如果内存使用超过 maxmemory,就会按照置换策略删除一些 key;
新的命令执行成功。
在 redis.conf 配置文件中,可以设置 maxmemory-policy 来设置内存淘汰方式:
maxmemory-policy allkeys-lru

LRU 算法

LRU 是 Least Recently Used 的缩写,表示最近最少使用。当内存不够的时候,每次添加一条数据,都需要抛弃一条最久时间没有使用的旧数据。
标准的 LRU 算法为了降低查找和删除元素的时间复杂度,一般采用 Hash 表和双向链表结合的数据结构,hash 表可以赋予链表快速查找到某个 key 是否存在链表中,同时可以快速删除、添加节点。而双向链表的查找时间复杂度是O(n),删除和插入是O(1),借助HashMap结构,可以使得查找的时间复杂度变成O(1),Hash表用来查询在链表中的数据位置,链表负责数据的插入。
当新数据插入到链表头部时有两种情况:
当链表中没有这个key,且链表满了,把链表尾部的数据丢弃掉,新加入的缓存直接加入到链表头中;
当链表中的某个缓存被命中时,直接把数据移到链表头部,原本在头节点的缓存就向链表尾部移动。
这样,经过多次Cache操作之后,最近被命中的缓存,都会存在链表头部的方向,没有命中的,都会在链表尾部方向,当需要替换内容时,由于链表尾部是最少被命中的,我们只需要淘汰链表尾部的数据即可。

Redis 的 LRU 算法:

实际上,Redis 使用的 LRU 算法其实是一种不可靠的 LRU 算法,它实际淘汰的键并不一定是真正最少使用的数据,它的工作机制是:
随机采集淘汰的 key,每次随机选出 5 个 key;
然后淘汰这 5 个 key 中最少使用的 key。
这5个key是默认的个数,具体的数值可以在 redis.conf 中配置:
maxmemory-samples 5
当 key 的采样取值越大的时候就会越接近真实的 LRU 算法,因为取值越大获取的数据越完整,淘汰中的数据就更加接近最少使用的数据。
这里其实涉及一个权衡问题:如果需要在所有的数据中搜索最符合条件的数据,那么一定会增加系统的开销,Redis 是单线程的,所以耗时的操作会谨慎一些。为了在一定成本内实现相对的 LRU,早期的 Redis 版本是基于采样的 LRU,也就是放弃了从所有数据中搜索解改为采样空间搜索最优解。Redis3.0 版本之后,Redis 作者对于基于采样的 LRU进行了一些优化:
Redis中维护一个大小为16的候选池,当第一次随机选取采用数据时,会把数据放入到候选池中,并且候选池中的数据会更具时间进行排序;
当第二次以后选取数据时,只有小于候选池内最小时间的才会被放进候选池;
当候选池的数据满了之后,那么时间最大的 key 就会被挤出候选池。当执行淘汰时,直接从候选池中选取最近访问时间小的 key 进行淘汰;

LRU 算法的缺点:

LRU 算法有一个弊端,加入一个 key 值访问频率很低,但是最近一次被访问到了,那LRU 会认为它是热点数据,不会被淘汰。同样,经常被访问的数据,最近一段时间没有被访问,这样会导致这些数据被淘汰掉,导致误判而淘汰掉热点数据,于是在 Redis 4.0 中,新加了一种 LFU 算法。

LFU 算法

LFU(Least Frequently Used),表示使用次数最少。它和 key 的使用次数有关,其思想是:根据 key 最近被访问的频率进行淘汰,比较少访问的 key 优先淘汰,反之则保留。
LRU 的原理是使用计数器来对 key 进行排序,每次 key 被访问时,计数器会增大,当计数器越大,意味着当前 key 的访问越频繁,也就是意味着它是热点数据。 很好的解决了 LRU 算法的缺陷:一个很久没有被访问的key,偶尔被访问一次,导致被误认为是热点数据的问题。
LFU 维护了两个链表,横向组成的链表用来存储访问频率,每个访问频率的节点下存储另外一个具有相同访问频率的缓存数据。具体的工作原理是:
当添加元素时,找到相同访问频次的节点,然后添加到该节点的数据链表的头部。如果该数据链表满了,则移除链表尾部的节点
当获取元素或者修改元素是,都会增加对应key的访问频次,并把当前节点移动到下一个频次节点。
添加元素时,访问频率默认为1,随着访问次数的增加,频率不断递增。而当前被访问的元素也会随着频率增加进行移动。

总结

Redis 过期删除策略是采用惰性删除和定期删除这两种方式组合进行的,惰性删除能够保证过期的数据我们在获取时一定获取不到,而定期删除设置合适的频率,则可以保证无效的数据及时得到释放,而不会一直占用内存数据。
但由于 Redis 是部署在物理机上的,内存不可能无限扩充的,当内存达到我们设定的界限后,便自动触发 Redis 内存淘汰策略,而具体的策略方式要根据实际业务情况进行选取。

标签:删除,过期,Redis,链表,内存,key
From: https://www.cnblogs.com/DCFV/p/18315759

相关文章

  • 在K8S中,Pod占用内存和cpu较高,该如何解决?
    在Kubernetes(K8s)中,当Pod占用内存和CPU较高时,可以通过一系列步骤来诊断并解决问题,以确保集群的稳定性和性能。以下是详细的解决步骤:1.监控和诊断使用kubectl命令:使用kubectltoppods命令查看集群中各个Pod的CPU和内存占用情况,找出占用资源高的Pod。使用kubectldescribepod......
  • 本地训练 Llama-2-7b-hf 模型时 CUDA 内存不足
    我想在我的笔记本电脑上本地微调meta-llama/Llama-2-7b-hf。实例化Trainer类时,我的CUDA内存不足。我有16Gb系统RAM和带有6GbGPU内存的GTX1060。我已将模型层拆分为CPU和GPU,以防止GPU填满并使用小批量大小。然而,当实例化训练器类时,代码再次尝试填充GPU,但内存不......
  • Redis底层数据结构-简单动态字符串SDS
    简单动态字符串(simpledynamicstring,SDS)。Redis没有直接使用C语言传统的字符串,而是自己构建了一种简单动态字符串(SDS)的抽象类型。C字符串只会作为字符串字面量(stringliteral)用在一些无须对字符串值进行修改的地方。实现sds.h/sdshdrstruct__attribute__((__packed__)......
  • Java性能优化-String的intern()方法的使用减少内存消耗
    场景String.intern()String.intern()方法用于在字符串常量池中查找是否存在与指定字符串相等的字符串。如果找到了,就返回该字符串的引用;否则,就在字符串常量池中创建一个新的字符串对象,并返回对它的引用。这个方法对于避免创建重复的字符串对象非常有用,特别是在处理大量字符串......
  • Robot Operating System——借用内存型消息
    大纲功能和工作原理源码分析POD特点POD类型的优点非POD特点生成并发布“借用内存型消息”POD类型非POD类型在ROS2中,"loanedmessage"是一种消息传递机制,用于在发布者(publisher)和订阅者(subscriber)之间传递数据。它是一种高效的消息传递方式,可以避免不必要的数据......
  • 记一次 Redisson 线上问题 → 你怎么能释放别人的锁
    开心一刻今天,我的又一个好哥们脱单了,只剩下我自己单身了我向一个我喜欢的女生吐苦水我:我这辈子是找不到女朋友了她:怎么可能,你很优秀的,会有很多女孩子愿意当你女朋友的我内心窃喜,问道:那你愿意当我女朋友吗她:我都在开导你了,你不要恩将仇报!线上问题生产环境突然告警,告警信......
  • redis事务是否支持原子性
    ACID中关于原子性的定义:原子性:一个事务(transaction)中的所有操作,或者全部完成,或者全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。即,事务不可分割、不可约简。Redis事务不支持传统意义上的原子......
  • ffmpeg内存模型
    FFmpeg内存模型在FFmpeg中,当从现有的Packet拷贝一个新Packet时,有两种情况:数据共享:两个Packet的buf引用的是同一数据缓存空间。这种情况下,需要注意数据缓存空间的释放问题。一个数据块同时被两个AVPacket(avpacket1和avpacket2)引用。此时,两者持有的是同一数据。数据独立:两个P......
  • Redis入门介绍
    目录Redis简介​编辑Redis下载与安装Redis服务启动与停止Redis数据类型字符串操作命令哈希操作命令列表操作命令集合操作命令有序集合操作命令通用命令在Java中操作RedisRedis的Java客户端SpringDataRedis使用方式 Redis简介Redis是一个基于内存的key-va......
  • # Redis 入门到精通(九)-- 主从复制
    Redis入门到精通(九)--主从复制(1)一、redis主从复制–主从复制简介1、互联网“三高”架构高并发高性能高可用2、你的“Redis”是否高可用?1)单机redis的风险与问题问题1.机器故障现象:硬盘故障、系统崩溃本质:数据丢失,很可能对业务造成灾难性打击结论:基本上会......