Skip to main content

Redis如何实现到期删除的?

作者:程序员马丁

在线博客:https://open8gu.com

note

大话面试,技术同学面试必备的八股文小册,以精彩回答应对深度问题,助力你在面试中拿个offer。

答题思路

回答话术

在 Redis 中,我们可以用 EXPIREPEXPIREEXPIREATPEXPIREAT四个命令来按毫秒或秒设置 Key 的过期时间。其中,前两者指定的是 Key 的有效时间,而后两者指定的是 Key 的到期时间点。

这些时间最终会被转换为一个时间戳并,与 Key 一一对应保存在一个到期字典中,然后 Redis 会根据 Key 在到期字典中的到期时间,通过主动和被动两种方式清理到期的 Key。

被动删除是指每次访问 Key 键时,Redis 会检查 Key 是否已到期,如果是就将其删除并返回空值。不过如果仅靠被动删除是不够的,因为如果 Key 的访问频率不高,可能会导致一些数据一直不能被删除,内存也无法得到释放,因此所以还需要定期的主动删除。

主动删除是指 Redis 会每秒主动扫描 10 次到期字典,随机抽取 20 个 Key 并删除其中已经到期的部分。然后,如果这次抽样中到期键的 Key 的比例超过 25%,就会继续抽样,直到不满足条件或超时为止。

以上两种删除机制互相配合,基本能保证 Redis 中到期键的数量不会超过总数据量的 25%。

另外,Redis 在持久化的时候也会针对到期的 Key 做额外的处理。Redis 在 AOF 的时候,如果 Key 过期了,则会向文件追加一条 DEL 指令,而如果是在 AOF 重写和 RDB 的时候,则检查并直接忽略掉过期的 Key。

最后是集群,在集群里面,当主节点发现 Key 到期时,会向所有从节点发送 DEL 命令,但是当从节点发现键到期时,只会将其标记为已删除,直到收到主节点的删除指令才会真正删除,以确保数据一致性。

问题详解

1. 到期时间

在 Redis 中,你可以使用以下四种命令为 Key 设置到期时间:

  • EXPIRE:以为单位,设置 Key 的有效时间。
  • PEXPIRE:以毫秒为单位,设置 Key 的有效时间。
  • EXPIREAT:以为单位,设置 Key 的到期时间戳。
  • PEXPIREAT:以毫秒为单位,设置 Key 的到期时间戳。

其中,前两者指定的是 Key 的有效时长,而后两者指定的是 Key 到期时间点

不过,在 Redis 底层实现中,四种命令最终都会变为 Key 到期时间点对应的时间戳,并被记录在一个到期字典中(哈希表)。

2. Redis 的删除策略

按官方文档的说法,Redis 的过期删除有两种方式

  • 主动删除:每 10 秒扫描一次数据库,随机抽 20 个 key,并删除其中到期的 key。如果到期 Key 占比超过 25%,那么继续抽样,直到不满足条件或超时为止;
  • 被动删除:访问 Key 时检查到期时间,如果已经到期就删除;

官方文档对此进行了解释:Redis 官网 -- EXPIRE 指令介绍

当然,为了面试的时候有更多可说的,我们可以适当的扩展一下,介绍一下常见的几种实现方式,和它们的优缺点:

2.1. 定时删除

这里我们顺便提一下一个 Redis 没有使用,而非常简单粗暴的思路,那就是定时删除。

简单的来说,就是在设置 Key 的到期时间时,一并设置一个定时事件,等到事件触发时删除 key。

  • 优点: 可以及时释放资源,确保过期键能够被及时删除。
  • 缺点: 频繁的删除操作可能会占用大量的 CPU 时间。

总的来说,这个策略对内存优化而对 CPU 不友好,在 CPU 紧张而内存宽裕的场景中,它会将更多的 CPU 资源花费到没那么紧要的删除到期 Key 操作上。

综合上述考量, Redis 并没有使用这种方式

2.2. 惰性删除

惰性删除就是 Redis 提到的被动删除。

被动删除不会主动的删除到期的 key,而是当访问 Key 时再检查是否到期,如果到期了再将其删除。

它的优缺点与定时删除刚好相反:

  • 优点: 只在取出键时进行检查,避免了频繁的删除操作。
  • 缺点: 可能会导致内存积压问题。

惰性删除对 CPU 最友好,但是对内存就不友好了。尤其是当你需要在 Redis 中存放大量具备到期时间且不需要频繁访问的数据时,会造成内存积压。

2.3. 定期删除

定期删除就是 Redis 提到的主动删除。

具体来说,整个过程如下:

  1. Redis 定时(取决于 hz配置,默认为 10,即每秒 10 次)依次遍历 16 个数据库:
    1. 如果此时到期字典可用率较低,考虑到哈希冲突严重时链表可能很长,遍历需要额外的时间成本,那么将会直接跳过该数据库,等到下一次循环其重哈希以后再进行处理。
    2. 如果字典可用率处于正常水平,那么就依次从数据库的到期字典中对 Key 进行抽样(20 个),并删除已超时的 Key;
  2. 如果此次抽样中,到期 Key 的占比高于一定阈值(25%),则会再进行一次抽样删除,直到到期 Key 占比没那么高。或者本次任务执行超时为止;
  3. 如果此次执行超时了,那么将会记录当前处理的数据库下标,然后下次进行抽样时就直接从当前数据库开始执行,如此反复。

相对于定时删除和惰性删除,定期删除在内存和 CPU 消耗中取得了一个比较好的平衡

另外,使用抽样避免全量操作的思想在 Redis 中挺常见的,比如内存淘汰策略中的近似 LRU 和 LFU,具体可以参见文档:✅ Redis 常用内存淘汰策略?

2.3.1. 为什么要抽样而不是全量检查?

每次大批量的筛选并删除 Key 是十分消耗性能的,并且长时间的阻塞 Redis 的主线程还会降低吞吐量。为了避免上述的问题,Redis 需要通过控制筛选范围降低 Key 的数量,从而来提高操作性能并降低阻塞的时间。

当然,即便如此,如果你为大量的 Key 设置了接近的超时时间,那么当它们同时失效 —— 我们一般称呼这种常见为缓存雪崩 —— 时,由于每次抽样中到期 Key 占比都会高于 25% 的概率极高,那么 Redis 依然不得不花费更多的时间来删除大量的 Key。虽然有一个整体超时时间来兜底,但是为了提高效率,还是最好通过设置不同的超时时间之类的操作尽可能避免这种情况。

关于这部分内容,可以参见:✅ 如何解决缓存雪崩?

2.3.2. 如何控制定期删除的触发频率?

一般来说,我们可以通过配置文件中的 hz 参数来指定定期删除任务的触发频率,它默认为 10,即每 1s 执行 10 次,最大可以调成 500。

需要注意的是,这个参数实际上不止用于控制定期删除的执行频率,redis 中几乎所有定期执行的后台任务都通过该值来设置 —— 比如关闭超时的客户端连接,或者更新统计信息之类的 —— 因此,调大这个值会导致 Redis 服务占用更多的 CPU 资源。不过,如果你的 Redis 服务会有大量的 ttl 极短的 Key,那么你可以适量的调大 hz参数来及时清理掉这些朝生夕死的 Key。

总而言之,这是又是一个 CPU 和内存占用的权衡问题,不过 Redis 也提供了 dynamic-hz 的配置,当你设置为 yes 的时候,Redis 会根据情况适当的根据 hz 调整实际的触发频率,这个配置默认都是开启的。

这里我们直接放上配置文件,你可以结合注释来感受一下:

# Redis调用一个内部函数来执行许多后台任务,比如
# 关闭超时的客户端连接,清理从未被请求的过期键等等。
#
# 并非所有任务的执行频率都相同,但Redis根据指定的“hz”值检查要执行的任务。
#
# 默认情况下,“hz”设置为10。增加这个值会在Redis空闲时使用更多CPU资源,
# 但同时会使得Redis在有许多键同时过期时响应更快,并且超时处理可能会更精确。
#
# 可选值范围在1到500之间,但通常超过100不是一个好主意。大多数用户应该使用默认值10,
# 在需要非常低延迟的环境中可以将其提高到100。

hz 10

# 通常情况下,拥有与连接客户端数量成比例的HZ值是有用的。
# 例如,这对于避免在每次后台任务调用中处理过多客户端以避免延迟峰值很有用。
#
# 由于默认情况下HZ值保守地设置为10,Redis提供并默认启用了使用自适应HZ值的能力,
# 当有许多连接的客户端时会临时提高HZ值。
#
# 启用动态HZ时,实际配置的HZ将作为基线使用,但一旦连接更多客户端,
# 将根据需要使用配置的HZ值的倍数。这样,空闲实例将使用非常少的CPU时间,而繁忙实例将更具响应性。

dynamic-hz yes

3. 在持久化时

Redis 使用 AOF 与 RBD 两种方式来持久化内存中数据,这个过程同样需要考虑如何处理过期的 key:

  • AOF:当 Key 因为到期而被删除时,将会向 AOF 追加一条 **DEL** 命令。如果在这个过程中进行了 AOF 重写,那么重写后的 AOF 文件中则将直接忽略掉这个过期的 Key。
  • RDB:与 AOF 重写类似,在创建 RDB 的时候,过期的 Key 会被直接忽略

具体关于 AOF 与 RDB 相关内容,可以参见文章:✅ Redis 宕机数据会丢失么?

4. 在集群中

当集群中的实例发现 Key 到期后,实例会根据它自己是主节点还是从节点而采取不同的行为:

  • 如果是主节点,它会在删除这个过期 Key 后向所有从节点发送一个 DEL 命令。
  • 如果是从节点,那么它将会将这个 Key 标记为到期,但并不会真正的删除。只有当接到从主节点发来的 DEL 命令之后,才会真正的将过期键删除掉。

从节点不会主动删除 key,这是为了保证与主节点数据的一致性,以便当主从切换时后,仍然可以正常的处理过期 key。

不过当系统中有大量频繁过期的 key,且一个主节点有较多从节点的时候,这会带来更多的内存消耗。

关于主从集群中到期时间的处理,可以参照官方文档 Redis replication sourl.cn/HRJygR 这部分内容。

不过在文档中似乎并没有明确指出缺少这种“延迟删除”的措施会导致怎样的后果,只说是为了不违反数据一致性 (don't violate the consistency of the data set)。