[Redis]过期删除和内存淘汰
过期删除
Redis 提供了四个命令来设置过期时间(生存时间):
- EXPIRE
:表示将键 key 的生存时间设置为 ttl 秒; - PEXPIRE
:表示将键 key 的生存时间设置为 ttl 毫秒; - EXPIREAT
:表示将键 key 的生存时间设置为 timestamp 所指定的秒数时间戳; - PEXPIREAT
:表示将键 key 的生存时间设置为 timestamp 所指定的毫秒数时间戳。
在Redis内部实现中,前面三个设置过期时间的命令最后都会转换成最后一个PEXPIREAT 命令来完成。
PERSIST
查看键的剩余过期时间:
TTL
PTTL
在 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 算法:#
redisobject中的lru字段
lru 字段是一个时间戳,记录了对象上次被访问的时间。在 Redis 内部,这个时间戳以服务器时钟的计数(一般是毫秒级别)表示。每次对象被访问时,这个字段都会更新。
实际上,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 优先淘汰,反之则保留。LFU 的原理是使用计数器来对 key 进行排序,每次 key 被访问时,计数器会增大,当计数器越大,意味着当前 key 的访问越频繁,也就是意味着它是热点数据。 很好的解决了 LRU 算法的缺陷:一个很久没有被访问的key,偶尔被访问一次,导致被误认为是热点数据的问题。
LFU 维护了两个链表,横向组成的链表用来存储访问频率,每个访问频率的节点下存储另外一个具有相同访问频率的缓存数据。具体的工作原理是:
- 当添加元素时,找到相同访问频次的节点,然后添加到该节点的数据链表的头部。如果该数据链表满了,则移除链表尾部的节点
- 当获取元素或者修改元素是,都会增加对应key的访问频次,并把当前节点移动到下一个频次节点。
- 添加元素时,访问频率默认为1,随着访问次数的增加,频率不断递增。而当前被访问的元素也会随着频率增加进行移动。
总结#
Redis 过期删除策略是采用惰性删除和定期删除
这两种方式组合进行的,惰性删除能够保证过期的数据我们在获取时一定获取不到,而定期删除设置合适的频率,则可以保证无效的数据及时得到释放,而不会一直占用内存数据。
但由于 Redis 是部署在物理机上的,内存不可能无限扩充的,当内存达到我们设定的界限后,便自动触发 Redis 内存淘汰策略,而具体的策略方式要根据实际业务情况进行选取。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· DeepSeek “源神”启动!「GitHub 热点速览」
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· NetPad:一个.NET开源、跨平台的C#编辑器