深入探讨Redis为何不立即删除过期数据的原因与机制
在面试中,关于Redis的内存管理及其过期数据处理常常是考察求职者的一道有趣题目。本文将深入总结这方面的内容,共包括四个主要问题:
- Redis为何要为缓存数据设置过期时间?
- Redis使用何种方式判断数据是否过期?
- 你了解Redis的过期键删除策略吗?
- 大量键在同一时间过期后如何处理?
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是如何对这些键进行删除的?
常见的过期数据删除策略有以下几种:
- 惰性删除:仅在访问/查询键时才对数据进行过期检查。这种方式对CPU友好,但可能会导致过多的过期键未被删除。
- 定期删除:周期性从设置了过期时间的键中随机抽查一批,然后逐个检查这些键是否过期,若过期则删除。相较于惰性删除,定期删除对内存更友好但对CPU的消耗较大。
- 延迟队列:将设置过期时间的键放入延迟队列,到期后删除。这种方式能确保所有过期键都会被删除,但维护延迟队列的成本较高,且队列本身也需要占用资源。
- 定时删除:每个设置了过期时间的键会在设置时间到达时立即被删除。这种方法能确保内存中不包含过期的键,但对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设计这种策略旨在平衡内存与性能之间的关系。
为何过期的键不立即删除?这样不是会造成内存浪费吗?
这主要是由于立即删除的成本过高。例如,如果使用延迟队列作为删除策略,可能会面临以下问题:
- 队列本身的开销可能很大:若键数量过多,一个延迟队列可能无法容纳。
- 维护延迟队列较为复杂:修改键的过期时间需要调整在延迟队列中的位置,并引入并发控制。
大量键集中过期如何处理?
若存在大量键同一时间过期的情况,可能会导致Redis的请求延迟增加。可以考虑以下可选方案来应对:
- 尽量避免键集中过期,在设置键的过期时间时尽量做到随机分布。
- 对过期的键启用lazyfree机制(通过修改
redis.conf
中的lazyfree-lazy-expire
参数),在后台异步删除过期键,避免阻塞主线程的执行。