深入探讨Redis为何不立即删除过期数据的原因与机制

在面试中,关于Redis的内存管理及其过期数据处理常常是考察求职者的一道有趣题目。本文将深入总结这方面的内容,共包括四个主要问题:

  1. Redis为何要为缓存数据设置过期时间?
  2. Redis使用何种方式判断数据是否过期?
  3. 你了解Redis的过期键删除策略吗?
  4. 大量键在同一时间过期后如何处理?

Redis为何要为缓存数据设置过期时间?

在通常情况下,当我们保存缓存数据时,都会指定一个过期时间。这是因为内存资源是有限且宝贵的。如果不为缓存数据设定过期时间,内存使用会不断增加,最终可能导致OOM(内存溢出)问题。通过合理设置过期时间,Redis可以自动清理暂时不需要的数据,从而为新缓存数据释放空间。

Redis提供了设置缓存数据过期时间的内建功能,例如:

127.0.0.1:6379> expire key 60  # 数据将在60秒后过期  
(integer) 1  
127.0.0.1:6379> setex key 60 value  # 数据将在60秒后过期 (setex: [set] + [ex]pire)  
OK  
127.0.0.1:6379> ttl key  # 查看数据还剩多少过期时间  
(integer) 56  

注意:在Redis中,除了字符串类型有独特的设置过期时间的命令setex外,其他类型的数据都需使用expire命令来设置过期时间。此外,persist命令可用于移除某个键的过期时间。

设定过期时间除了有助于减少内存消耗外,还有其他作用吗?

在很多业务场景中,我们需要某些数据仅在特定时间段内有效。例如,短信验证码通常在1分钟内有效,用户登录的Token有效期可能为1天。如果使用传统数据库处理这类情况,通常需要自行判断过期,这样做既麻烦又性能较差。

Redis使用何种方式判断数据是否过期?

Redis通过一个称为过期字典(可视为哈希表)来保存数据的过期时间。过期字典的键指向Redis数据库中的某个键,而字典的值是一个long long类型的整数,表示该键的过期时间(以毫秒为单位的UNIX时间戳)。

图片

过期字典存储在redisDb结构中:

typedef struct redisDb {  
    ...  
    dict *dict;     // 数据库键空间,保存数据库中的所有键值对  
    dict *expires   // 过期字典,保存键的过期时间  
    ...  
} redisDb;  

当查询一个键时,Redis首先检查该键是否存在于过期字典中(时间复杂度为O(1))。如果不存在,则直接返回;如果存在,则需判断该键是否过期,若过期则直接删除该键并返回null。

你了解Redis的过期键删除策略吗?

假设你设置了一批键只能存活1分钟,那么在1分钟后,Redis是如何对这些键进行删除的?

常见的过期数据删除策略有以下几种:

  1. 惰性删除:仅在访问/查询键时才对数据进行过期检查。这种方式对CPU友好,但可能会导致过多的过期键未被删除。
  2. 定期删除:周期性从设置了过期时间的键中随机抽查一批,然后逐个检查这些键是否过期,若过期则删除。相较于惰性删除,定期删除对内存更友好但对CPU的消耗较大。
  3. 延迟队列:将设置过期时间的键放入延迟队列,到期后删除。这种方式能确保所有过期键都会被删除,但维护延迟队列的成本较高,且队列本身也需要占用资源。
  4. 定时删除:每个设置了过期时间的键会在设置时间到达时立即被删除。这种方法能确保内存中不包含过期的键,但对CPU的压力最大,因为每个键都需设置一个定时器。

Redis采用哪种删除策略呢?

Redis结合了定期删除+惰性/懒汉式删除的策略,这是大多数缓存框架常用的选择。定期删除对内存友好,而惰性删除则对CPU友好。两者结合使用既能兼顾CPU性能,又能优化内存资源的利用。

接下来,我们将详细介绍Redis中的定期删除是如何进行的。

Redis的定期删除过程是随机的(周期性随机从设置了过期时间的键中抽查一批),因此并不能保证所有过期键都会立即被删除。这也解释了为何某些键过期后并未被删除。此外,Redis的底层会通过限制删除操作执行的时间和频率来减少这些操作对CPU时间的影响。

定期删除还会受到执行时间和过期键比例的影响:

  • 若执行时间超过阈值,则中断当前定期删除循环,以避免消耗过多CPU时间。
  • 如果当前批次过期键的比例超过某个阈值,则会重复执行删除流程,以更积极地清理过期键。相反,若过期键比例低于该阈值,则中断当前循环,以避免过度工作而回收内存有限。

在Redis 7.2版本中,执行时间的阈值设定为25ms,过期键的比例阈值则为10%

#define ACTIVE_EXPIRE_CYCLE_FAST_DURATION 1000  /* Microseconds. */  
#define ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 25  /* Max % of CPU to use. */  
#define ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE 10  /* % of stale keys after which  
                                                     we do extra efforts. */  

每次随机抽查的数量是?

根据expire.c的定义,在Redis 7.2版本中,每次随机抽查的键数量为20,即每次会随机选择20个设置了过期时间的键进行过期判断:

#define ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP 20  /* Keys for each DB loop. */  

如何控制定期删除的执行频率?

在Redis中,定期删除的频率由hz参数控制。该参数默认为10,意味着每秒执行10次,即每秒钟进行10次尝试以查找并删除过期的键。

hz的取值范围是1~500。增大hz参数的值可以提升定期删除的频率。如果希望更频繁地执行此任务,可以适当增加hz的值,但需注意这会增加CPU的使用率。根据Redis官方的建议,hz的值不应超过100,对于大多数用户而言,默认的10就已足够。

以下是hz参数的官方注释,翻译了其中的重要信息(Redis 7.2版本):

图片

在Redis配置文件redis.conf中,这两个参数的设置如下:

# 默认为10  
hz 10  
# 默认开启  
dynamic-hz yes  

另外,除了定期删除过期键的任务外,还有其他定期任务,例如关闭超时的客户端连接、更新统计信息等,这些任务的执行频率也由hz参数控制。

为何定期删除不会一次性删除所有过期键?

这样做将对性能造成极大影响。如果键的数量极为庞大,挨个遍历检查将非常耗时,严重影响性能。Redis设计这种策略旨在平衡内存与性能之间的关系。

为何过期的键不立即删除?这样不是会造成内存浪费吗?

这主要是由于立即删除的成本过高。例如,如果使用延迟队列作为删除策略,可能会面临以下问题:

  1. 队列本身的开销可能很大:若键数量过多,一个延迟队列可能无法容纳。
  2. 维护延迟队列较为复杂:修改键的过期时间需要调整在延迟队列中的位置,并引入并发控制。

大量键集中过期如何处理?

若存在大量键同一时间过期的情况,可能会导致Redis的请求延迟增加。可以考虑以下可选方案来应对:

  1. 尽量避免键集中过期,在设置键的过期时间时尽量做到随机分布。
  2. 对过期的键启用lazyfree机制(通过修改redis.conf中的lazyfree-lazy-expire参数),在后台异步删除过期键,避免阻塞主线程的执行。