面试官:为什么 Redis 不立刻删除已经过期的数据?

JavaGuide

共 5685字,需浏览 12分钟

 ·

2024-06-19 14:07

JavaGuide官方网站javaguide.cn


这是一道比较有意思的 Redis 面试题,考察求职者对于 Redis 内存管理的了解。

我简单完善了一下 JavaGuide( javaguide.cn )对于这部分内容的总结,分享一下, 共计 4 个问题:

  1. Redis 给缓存数据设置过期时间有什么用?
  2. Redis 是如何判断数据是否过期的呢?
  3. Redis 过期 key 删除策略了解么?
  4. 大量 key 集中过期怎么办?

Redis 给缓存数据设置过期时间有什么用?

一般情况下,我们设置保存的缓存数据的时候都会设置一个过期时间。为什么呢?

内存是有限且珍贵的,如果不对缓存数据设置过期时间,那内存占用就会一直增长,最终可能会导致 OOM 问题。通过设置合理的过期时间,Redis 会自动删除暂时不需要的数据,为新的缓存数据腾出空间。

Redis 自带了给缓存数据设置过期时间的功能,比如:

127.0.0.1:6379> expire key 60 # 数据在 60s 后过期
(integer) 1
127.0.0.1:6379> setex key 60 value # 数据在 60s 后过期 (setex:[set] + [ex]pire)
OK
127.0.0.1:6379> ttl key # 查看数据还有多久过期
(integer) 56

注意 ⚠️:Redis 中除了字符串类型有自己独有设置过期时间的命令 setex 外,其他方法都需要依靠 expire 命令来设置过期时间 。另外, persist 命令可以移除一个键的过期时间。

过期时间除了有助于缓解内存的消耗,还有什么其他用么?

很多时候,我们的业务场景就是需要某个数据只在某一时间段内存在,比如我们的短信验证码可能只在 1 分钟内有效,用户登录的 Token 可能只在 1 天内有效。

如果使用传统的数据库来处理的话,一般都是自己判断过期,这样更麻烦并且性能要差很多。

Redis 是如何判断数据是否过期的呢?

Redis 通过一个叫做过期字典(可以看作是 hash 表)来保存数据过期的时间。过期字典的键指向 Redis 数据库中的某个 key(键),过期字典的值是一个 long long 类型的整数,这个整数保存了 key 所指向的数据库键的过期时间(毫秒精度的 UNIX 时间戳)。

Redis 过期字典

过期字典是存储在 redisDb 这个结构里的:

typedef struct redisDb {
    ...

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

在查询一个 key 的时候,Redis 首先检查该 key 是否存在于过期字典中(时间复杂度为 O(1)),如果不在就直接返回,在的话需要判断一下这个 key 是否过期,过期直接删除 key 然后返回 null。

Redis 过期 key 删除策略了解么?

如果假设你设置了一批 key 只能存活 1 分钟,那么 1 分钟后,Redis 是怎么对这批 key 进行删除的呢?

常用的过期数据的删除策略就下面这几种:

  1. 惰性删除:只会在取出/查询 key 的时候才对数据进行过期检查。这种方式对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。
  2. 定期删除:周期性地随机从设置了过期时间的 key 中抽查一批,然后逐个检查这些 key 是否过期,过期就删除 key。相比于惰性删除,定期删除对内存更友好,对 CPU 不太友好。
  3. 延迟队列:把设置过期时间的 key 放到一个延迟队列里,到期之后就删除 key。这种方式可以保证每个过期 key 都能被删除,但维护延迟队列太麻烦,队列本身也要占用资源。
  4. 定时删除:每个设置了过期时间的 key 都会在设置的时间到达时立即被删除。这种方法可以确保内存中不会有过期的键,但是它对 CPU 的压力最大,因为它需要为每个键都设置一个定时器。

Redis 采用的那种删除策略呢?

Redis 采用的是 定期删除+惰性/懒汉式删除 结合的策略,这也是大部分缓存框架的选择。定期删除对内存更加友好,惰性删除对 CPU 更加友好。两者各有千秋,结合起来使用既能兼顾 CPU 友好,又能兼顾内存友好。

下面是我们详细介绍一下 Redis 中的定期删除具体是如何做的。

Redis 的定期删除过程是随机的(周期性地随机从设置了过期时间的 key 中抽查一批),所以并不保证所有过期键都会被立即删除。这也就解释了为什么有的 key 过期了,并没有被删除。并且,Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响。

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

  • 执行时间已经超过了阈值,那么就中断这一次定期删除循环,以避免使用过多的 CPU 时间。
  • 如果这一批过期的 key 比例超过一个比例,就会重复执行此删除流程,以更积极地清理过期 key。相应地,如果过期的 key 比例低于这个比例,就会中断这一次定期删除循环,避免做过多的工作而获得很少的内存回收。

Redis 7.2 版本的执行时间阈值是 25ms,过期 key 比例设定值是 **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 个设置了过期时间的 key 判断是否过期。

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

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

在 Redis 中,定期删除的频率是由 hz 参数控制的。hz 默认为 10,代表每秒执行 10 次,也就是每秒钟进行 10 次尝试来查找并删除过期的 key。

hz 的取值范围为 1~500。增大 hz 参数的值会提升定期删除的频率。如果你想要更频繁地执行定期删除任务,可以适当增加 hz 的值,但这会加 CPU 的使用率。根据 Redis 官方建议,hz 的值不建议超过 100,对于大部分用户使用默认的 10 就足够了。

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

redis.conf 对于 hz 的注释

类似的参数还有一个 dynamic-hz,这个参数开启之后 Redis 就会在 hz 的基础上动态计算一个值。Redis 提供并默认启用了使用自适应 hz 值的能力,

这两个参数都在 Redis 配置文件 redis.conf中:

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

多提一嘴,除了定期删除过期 key 这个定期任务之外,还有一些其他定期任务例如关闭超时的客户端连接、更新统计信息,这些定期任务的执行频率也是通过 hz 参数决定。

为什么定期删除不是把所有过期 key 都删除呢?

这样会对性能造成太大的影响。如果我们 key 数量非常庞大的话,挨个遍历检查是非常耗时的,会严重影响性能。Redis 设计这种策略的目的是为了平衡内存和性能。

为什么 key 过期之后不立马把它删掉呢?这样不是会浪费很多内存空间吗?

因为不太好办到,或者说这种删除方式的成本太高了。假如我们使用延迟队列作为删除策略,这样存在下面这些问题:

  1. 队列本身的开销可能很大:key 多的情况下,一个延迟队列可能无法容纳。
  2. 维护延迟队列太麻烦:修改 key 的过期时间就需要调整期在延迟队列中的位置,并且,还需要引入并发控制。

大量 key 集中过期怎么办?

如果存在大量 key 集中过期的问题,可能会使 Redis 的请求延迟变高。可以采用下面的可选方案来应对:

  1. 尽量避免 key 集中过期,在设置键的过期时间时尽量随机一点。
  2. 对过期的 key 开启 lazyfree 机制(修改 redis.conf 中的 lazyfree-lazy-expire参数即可),这样会在后台异步删除过期的 key,不会阻塞主线程的运行。

⭐️推荐:


点击下方卡片进入公众号

回复 「PDF 即可领取原创PDF技术面试手册
回复 「学习路线 即可获取最新版Java学习路线
回复 「开源 即可获取优质Java开源项目合集
免费分享无套路,有帮助点个赞就好!


浏览 815
5点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
5点赞
评论
收藏
分享

手机扫一扫分享

分享
举报