redis过期键的删除策略
本文是学习黄健宏《Redis设计与实现》并根据源码整理了一点心得体会,特此记录。 主要记录对于单次函数执行时间限制的动态调整策略的实现。 回答部分摘抄了chatgpt 4.0
删除策略的分类
一般来说,一个键过期后,从逻辑上有三种策略可以进行删除:
- 定时删除:在设置键的过期时间的同时,创建一个定时器 ( timer ),让定时器在键的过期时间来临时,立即执行对键的删除操作。
- 惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。
- 定期删除:每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。在这三种策略中,第一种和第三种为主动删除策略,而第二种则为被动删除策略。
定时删除策略: 定时删除策略对内存是最友好的:通过使用定时器,定时删除策略可以保证过期键会尽可能快地被删除,并释放过期键所占用的内存。 另一方面,定时删除策略的缺点是,它对 CPU 时间是最不友好的:在过期键比较多的情况下,删除过期键这一行为可能会占用相当一部分 CPU 时间,在内存不紧张但是 CPU 时间非常紧张的情况下,将 CPU 时间用在删除和当前任务无关的过期键上,无疑会对服务器的响应时间和吞吐量造成影响。 例如,如果正有大量的命令请求在等待服务器处理,并且服务器当前不缺少内存,那么服务器应该优先将 CPU 时间用在处理客户端的命令请求上面,而不是用在删除过期键上面。除此之外,创建一个定时器需要用到 Redis 服务器中的时间事件,而当前时间事件的实现方式一-无序链表,查找一个事件的时间复杂度为 O(N) -- 并不能高效地处理大量时间事件。 因此,要让服务器创建大量的定时器,从而实现定时删除策略,在现阶段来说并不现实.
惰性删除: 惰性删除策略对 CPU 时间来说是最友好的:程序只会在取出键时才对键进行过期检查这可以保证删除过期键的操作只会在非做不可的情况下进行,并且删除的目标仅限于当前处理的键,这个策略不会在删除其他无关的过期键上花费任何 CPU 时间。 惰性删除策略的缺点是,它对内存是最不友好的:如果一个键已经过期,而这个键又仍然保留在数据库中,那么只要这个过期键不被删除,它所占用的内存就不会释放。 在使用惰性删除策略时,如果数据库中有非常多的过期键,而这些过期键又恰好没有被访问到的话,那么它们也许永远也不会被删除(除非用户手动执行 FLUSHDB),我们甚至可以将这种情况看作是一种内存泄漏一一无用的垃圾数据占用了大量的内存,而服务器却不会自己去释放它们,这对于运行状态非常依赖于内存的 Redis 服务器来说,肯定不是一个好消息。 举个例子,对于一些和时间有关的数据,比如日志(log),在某个时间点之后,对它们的访问就会大大减少,甚至不再访问,如果这类过期数据大量地积压在数据库中,用户以为服务器已经自动将它们删除了,但实际上这些键仍然存在,而且键所占用的内存也没有释放,那么造成的后果肯定是非常严重的。
定时删除: 从上面对定时删除和情性删除的讨论来看,这两种删除方式在单一使用时都有明显的缺陷:
- 定时删除占用太多 CPU 时间,影响服务器的响应时间和吞吐量。
- 惰性删除浪费太多内存,有内存泄漏的危险。 定期删除策略是前两种策略的一种整合和折中:
- 定期删除策略每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响。
- 除此之外,通过定期删除过期键,定期删除策略有效地减少了因为过期键而带来的内存浪费。 定期删除策略的难点是确定删除操作执行的时长和频率:
- 如果删除操作执行得太频繁,或者执行的时间太长,定期删除策略就会退化成定时删除策略,以至于将 CPU 时间过多地消耗在删除过期键上面。
- 如果删除操作执行得太少,或者执行的时间太短,定期删除策略又会和惰性删除策略一样,出现浪费内存的情况。 因此,如果采用定期删除策略的话,服务器必须根据情况,合理地设置删除操作的执行时长和执行频率。
-- 黄健宏《Redis设计与实现》2014.4月版
redis中的删除策略
在redis中,使用的是惰性删除和定期删除两种策略的结合。
惰性删除即在访问键时,先判断是否过期,过期的话就删除,然后按键不存在返回。
定期删除的伪代码如下:
c
void activeExpireCycle(int type) {
for (int i = 0; i < dbnum; i++) {
for (int j = 0; j < keys.length; j++) {
// TODO CLEAR
if (reach_time_limit()) {
break;
}
}
}
}
笔者使用的redis版本为6.0.6 activeExpireCycle是在服务端定时任务中调用的函数,服务器每秒调用定时任务的频率 可以参考配置中的hz
字段,在代码中用server.hz表示
本文主要介绍reach_time_limit是怎么计算的。
计算当前的时间开销这个比较简单,用当前时间减去函数开始时间即可。
复杂的是time_limit怎么动态计算。
在6.0.6中,有两种计算逻辑:
有两种计算time_limit的模式,根据传入的type类型决定。
- ACTIVE_EXPIRE_CYCLE_SLOW: 慢速过期键检查
- ACTIVE_EXPIRE_CYCLE_FAST: 快速过期键检查
ACTIVE_EXPIRE_CYCLE_SLOW, 慢速过期键检查策略
公式:
c
timelimit = config_cycle_slow_time_perc * 1000000 / server.hz / 100;
config_cycle_slow_time_perc:这个值表示在定期过期检查循环中,允许使用的最大 CPU 时间百分比。例如,如果 config_cycle_slow_time_perc 是 25,则意味着在每个循环中,最多可以使用 25% 的 CPU 时间来进行过期键的检查。
1000000:这个数字是将时间百分比转换为微秒的因子。由于有 1,000,000 微秒在一秒钟中,这个转换确保了时间限制是以微秒为单位。
server.hz:这是 Redis 服务器的心跳频率,即每秒钟的定时任务执行次数。比如,如果 server.hz 是 10,则意味着每秒钟有 10 次机会执行包括键过期检查在内的定时任务。
100:这是将百分比转换为小数的因子(例如,25% 变为 0.25)。
现在,让我们通过一个例子来计算 timelimit:
假设:
config_cycle_slow_time_perc = 25(即,最多使用 25% 的 CPU 时间) server.hz = 10(即,每秒钟执行 10 次定时任务) 计算 timelimit:
ini
timelimit = 25 * 1000000 / 10 / 100
= 25000000 / 10 / 100
= 2500000 / 100
= 25000 微秒
所以,在这个例子中,timelimit 是 25000 微秒,这意味着在定期删除的每个循环中,Redis 将花费最多 25000 微秒来检查和删除过期的键。这种方式平衡了性能和及时清理过期键的需要。
这里的CPU时间是指,CPU执行时间,计算的是占据单个定时任务执行时间的百分比。
例如 1秒钟,每秒执行10次定时任务,其中清除定时键最大占用百分之25。 config_cycle_slow_time_perc就是25 10就是server.hz
config_cycle_slow_time_perc的计算过程:
c
#define ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 25 /* Max % of CPU to use. */
effort = server.active_expire_effort-1, /* Rescale from 0 to 9. */
config_cycle_slow_time_perc = ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC + 2*effort,
后文会介绍effort的定义和使用场景。
ACTIVE_EXPIRE_CYCLE_FAST, 快速过期键检查策略
公式:
c
config_cycle_fast_duration = ACTIVE_EXPIRE_CYCLE_FAST_DURATION +
ACTIVE_EXPIRE_CYCLE_FAST_DURATION / 4 * effort,
其中:
- ACTIVE_EXPIRE_CYCLE_FAST_DURATION 定义为 1000 微秒。
- effort 是一个动态的值,通常在 1 到 10 之间。
在快速模式下,timelimit 被设置为 config_cycle_fast_duration。快速模式通常在特定条件下触发,比如大量的键同时过期。
effort为1时,值为1250,为10时,值为3500
动态负载值的设置
active_expire_effort 的值通常是在 Redis 服务器启动时通过配置决定的。这个值是一个配置参数,允许使用者根据他们对 Redis 服务器性能和过期键处理需求的预期来调整它。
active_expire_effort 参数影响的是 Redis 如何处理键的过期,尤其是在定期删除策略中。它调整了 Redis 分配给过期键检查任务的资源量,这包括了 CPU 时间和在每次过期键检查中要扫描的键的数量。
这个参数的调整通常基于以下几个因素:
-
服务器性能:如果服务器性能较高,可以通过增加 active_expire_effort 的值来让 Redis 更积极地检查和删除过期键,从而保持内存的有效使用。
-
负载情况:在负载较低时,可以提高这个值以快速清理过期键;而在负载较高时,可能需要降低这个值以避免对性能造成过大影响。
-
应用场景:不同的使用场景对过期键的处理需求不同。例如,对于需要快速响应和高吞吐量的应用,可能需要一个较高的 active_expire_effort 值。
最终,active_expire_effort 的设置是一个平衡过程,需要根据具体的使用情况和性能需求来决定。虽然这个值在服务器启动时设置,但是它也可以在运行时通过配置命令调整,从而允许对 Redis 的行为进行动态调整。