• 进入"运维那点事"后,希望您第一件事就是阅读“关于”栏目,仔细阅读“关于Ctrl+c问题”,不希望误会!

Redis Key过期删除机制

Redis 彭东稳 7年前 (2017-07-13) 34136次浏览 已收录 0个评论

Redis 是 k-v 数据库,我们可以设置 Redis 中缓存的 key 的过期时间。那么 Redis 缓存失效(key 过期)的故事要从 EXPIRE 这个命令说起,EXPIRE 命令允许用户为某个 key 指定其过期时间,当 key 超过这个时间后,我们应该就访问不到这个值了。接下来我们继续深入探究这个问题,Redis 缓存失效机制是如何实现的呢?也就是当 key 超过过期时间后 Redis 如何处理 key 呢?

过期策略通常有以下三种:

  • 定时过期:每个设置过期时间的 key 都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的 CPU 资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
  • 惰性过期:只有当访问一个 key 时,才会判断该 key 是否已过期,过期则清除。该策略可以最大化地节省 CPU 资源,却对内存非常不友好。极端情况可能出现大量的过期 key 没有再次被访问,从而不会被清除,占用大量内存。
  • 定期过期:每隔一定的时间,会扫描一定数量的数据库的 expires 字典中一定数量的 key,并清除其中已过期的 key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得 CPU 和内存资源达到最优的平衡效果。(expires 字典会保存所有设置了过期时间的 key 的过期时间数据,其中,key 是指向键空间中的某个键的指针,value 是该键的毫秒精度的 UNIX 时间戳表示的过期时间。键空间是指该 Redis 集群中保存的所有键。)

Redis 中同时使用了惰性过期和定期过期两种过期策略。

这篇文章主要在分析 Redis 源码的基础上站在 Redis 设计者的角度去思考 Redis 缓存失效的相关问题。

一、Redis Key过期机制

通常 Redis key 创建时没有设置相关过期时间。他们会一直存在,除非使用显示的命令移除,例如,使用 DEL 命令。EXPIRE 一类命令能关联到一个有额外内存开销的 key。当 key 执行过期操作时,Redis 会确保按照规定时间删除他们。key 的过期时间和永久有效性可以通过 EXPIRE 和 PERSIST 命令(或者其他相关命令)来进行更新或者删除过期时间。

为给定 key 设置生存时间,当 key 过期时(生存时间为 0),它会被自动删除。在 Redis 中,带有生存时间的 key 被称为『易失的』(volatile)。生存时间可以通过使用 DEL 命令来删除整个 key 来移除,或者被 SET 和 GET/SET 命令覆写(overwrite),这意味着,如果一个命令只是修改(alter)一个带生存时间的key的值而不是用一个新的 key 值来代替(replace)它的话,那么生存时间不会被改变。比如说,对一个 key 执行 INCR 命令,对一个列表进行 LPUSH 命令,或者对一个哈希表执行 HSET 命令,这类操作都不会修改 key 本身的生存时间。

另一方面,如果使用 RENAME 对一个 key 进行改名,那么改名后的 key 的生存时间和改名前一样。RENAME 命令的另一种可能是,尝试将一个带生存时间的 key 改名成另一个带生存时间的 another_key,这时旧的 another_key(以及它的生存时间)会被删除,然后旧的 key 会改名为 another_key,因此,新的 another_key 的生存时间也和原本的 key 一样。

使用 PERSIST 命令可以在不删除 key 的情况下,移除 key 的生存时间,让 key 重新成为一个『持久的』(persistent) key 。

NOTE

在小于 2.1.3 的 Redis 版本里,只能对 key 设置一次 expire。Redis 2.1.3 和之后的版本里,可以多次对 key 使用 expire 命令,更新 key 的 expire time。

Redis 术语里面,把设置了 expire time 的 key 叫做:volatile keys,意思就是不稳定的 key。

如果对 key 使用 set 或 del 命令,那么也会移除 expire time。尤其是 set 命令,这个在编写程序的时候需要注意一下。

Redis 2.1.3 之前的老版本里,如果对 volatile keys 做相关写入操作(LPUSH,LSET),和其他一些触发修改 value 的操作时,Redis 会删除该 key。

到此为止我们大概明白了什么是缓存失效机制以及缓存失效机制的一些应用场景,接下来我们继续深入探究这个问题,Redis缓存失效机制是如何实现的呢?

二、Redis key过期淘汰机制

Redis 服务器使用的是惰性删除和定期删除两种策略:通过配合使用这两种删除策略,服务器可以很好地在合理使用 CPU 时间和避免浪费内存空间之间取得平衡。

1. 惰性过期机制

惰性过期机制即当客户端请求操作某个 key 的时候,Redis 会对客户端请求操作的 key 进行有效期检查,如果 key 过期才进行相应的处理,惰性过期机制也叫消极失效机制。我们看看 t_string 组件下面对 get 请求处理的服务端端执行堆栈:

关键的地方是 expireIfNeed 函数,Redis 对 key 的 get 操作之前会判断 key 关联的值是否失效。expireIfNeeded 函数就像一个过滤器,它可以在命令真正执行之前,过滤掉过期的输入键,从而避免命令接触到过期键。

这里先插入一个小插曲,我们看看 Redis 中实际存储值的地方是什么样子的:

上面是 Redis 中定义的一个结构体,dict 是一个 Redis 实现的一个字典,也就是每个 DB 会包括上面的五个字段,我们这里只关心两个字典,一个是 dict,一个是 expires:

  1. dict 是用来存储正常数据的,比如我们执行了 set key “hahaha”,这个数据就存储在 dict 中。
  2. expires 使用来存储关联了过期时间的 key,比如我们在上面的基础之上有执行的 expire key 1,这个时候就会在 expires 中添加一条记录。

回过头来看看 expireIfNeeded 函数的流程,大致如下:

  • 如果输入键已经过期,那么 expireIfNeeded 函数将输入键从数据库中删除。
  • 如果输入键未过期,那么 expireIfNeeded 函数不做动作。

代码的注释已经很清晰了,总结一下:

  1. 从 expires 中查找 key 的过期时间,如果不存在说明对应 key 没有设置过期时间,直接返回 0。
  2. 如果是 slave 机器,则直接返回,因为 Redis 为了保证数据一致性且实现简单,将缓存失效的主动权交给 master 机器,slave 机器没有权限将 key 失效。
  3. 如果当前是 master 机器,且 key 过期,则 master 会做两件重要的事情:
    1. 将删除命令写入 AOF 文件。
    2. 通知 slave 当前 key 失效,可以删除了。
  4. master 从本地的字典中将 key 对应的值删除。

因为每个被访问的键都可能因为过期而被 expireIfNeeded 函数删除,所以每个命令的实现函数都必须能同时处理键存在以及键不存在这两种情况:

  • expireIfNeeded 函数返回 0,意味当键存在时,命令按照键存在的情况执行。
  • expireIfNeeded 函数返回 1,意味当键因为过期而被 expireIfNeeded 函数删除时,命令按照键不存在的情况执行。

2. 定期过期机制

定期过期机制也叫主动失效机制,即服务端定期的去检查失效的缓存,如果失效则进行相应的操作。

我们都知道 Redis 是单线程的,基于事件驱动的,Redis 中有个 EventLoop,EventLoop 负责对两类事件进行处理:

  1. 一类是 IO 事件,这类事件是从底层的多路复用器分离出来的。
  2. 一类是定时事件,这类事件主要用来对某个任务的定时执行。

看起来 Redis 的 EventLoop 一方面对网络 I/O 事件处理,一方面还可以做一些小任务。

为什么讲到 Redis 的单线程模型,因为 Redis 的主动失效机制逻辑是被当做一个定时任务来由主线程执行的,相关代码如下:

serverCron 就是这个定时任务的函数指针,adCreateTimeEvent 将 serverCron 任务注册到 EventLoop 上面,并设置初始的执行时间是 1 毫秒之后。接下来,我们想知道的东西都在 serverCron 里面了。serverCron 做的事情有点多,我们只关心和本篇内容相关的部分,也就是缓存失效是怎么实现的,我认为看代码做什么事情,调用堆栈还是比较直观的:

EventLoop 通过对定时任务的处理,周期性触发对 serverCron 逻辑的执行,最终执行 key 过期处理的逻辑,也就是调用 activeExpireCycle 函数。值得一提的是,activeExpireCycle 逻辑只能由 master 线程来做。它在规定的时间内,分多次遍历服务器中的各个数据库,从数据库的 expires 字典中随机检查一部分键的过期时间,并删除其中的过期键。

activeExpireCycle 函数的工作模式可以总结如下:

  • 函数每次运行时,都从一定数量的数据库中取出一定数量的随机键进行检查,并删除其中的过期键。
  • 全局变量 current_db 会记录当前 activeExpireCycle 函数检查的进度,并在下一次 activeExpireCycle 函数调用时,接着上一次的进度进行处理。比如说,如果当前 activeExpireCycle 函数在遍历 10 号数据库时返回了,那么下次 activeExpireCycle 函数执行时,将从 11 号数据库开始查找并删除过期键。
  • 随着 activeExpireCycle 函数的不断执行,服务器中的所有数据库都会被检查一遍,这时函数将 current_db 变量重置为 0,然后再次开始新一轮的检查工作。

另外,serverCron 周期性操作函数每 100ms 执行一次,还包括检查服务器状态,比如检查 “save N M” 是否满足条件,如果满足就执行 BGSAVE;当然也包括 AOF 重写检查等。

三、计算并返回剩余生存时间

TTL 命令以秒为单位返回键的剩余生存时间,而 PTTL 命令则以毫秒为单位返回键的剩余生存时间:

TTL 和 PTTL 两个命令都是通过计算键的过期时间和当前时间之间的差来实现的,以下是这两个命令的伪代码实现:

Keys 的过期时间使用 Unix 时间戳存储(从 Redis 2.6 开始以毫秒为单位)。这意味着即使 Redis 实例不可用,时间也是一直在流逝的。

即使正在运行的实例也会检查计算机的时钟,例如如果你设置了一个 key 的有效期是 1000 秒,然后设置你的计算机时间为未来 2000 秒,这时 key 会立即失效,而不是等 1000 秒之后。

四、AOF、RDB和复制功能对过期键的处理

生成 RDB 文件

在执行 SAVE 命令或者 BGSAVE 命令创建一个新的 RDB 文件时,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的 RDB 文件中。

例如,如果数据库中包含三个键 k1、k2、k3,并且 k2 已经过期,那么当执行 SAVE 命令或者 BGSAVE 命令时,程序只会将 k1 和 k3 的数据保存到 RDB 文件中,而 k2 则会被忽略。

因此,数据库中包含过期键不会对生成新的 RDB 文件造成影响。

载入 RDB 文件

在启动 Redis 服务器时,如果服务器开启了 RDB 功能,那么服务器将对 RDB 文件进行载入:

  • 如果服务器以主服务器模式运行,那么在载入 RDB 文件时,程序会对文件中保存的键进行检查,未过期的键会被载入到数据库中,而过期键则会被忽略,所以过期键对载入 RDB 文件的主服务器不会造成影响。
  • 如果服务器以从服务器模式运行,那么在载入 RDB 文件时,文件中保存的所有键,不论是否过期,都会被载入到数据库中。不过,因为主从服务器在进行数据同步的时候,从服务器的数据库就会被清空,所以一般来讲,过期键对载入 RDB 文件的从服务器也不会造成影响。

例如,如果数据库中包含三个键 k1、k2、k3,并且 k2 已经过期,那么当服务器启动时:

  • 如果服务器以主服务器模式运行,那么程序只会将 k1 和 k3 载入到数据库,k2 会被忽略。
  • 如果服务器以从服务器模式运行,那么 k1、k2 和 k3 都会被载入到数据库。

AOF 文件写入

当服务器以 AOF 持久化模式运行时,如果数据库中的某个键已经过期,但它还没有被惰性删除或者定期删除,那么 AOF 文件不会因为这个过期键而产生任何影响。当过期键被惰性删除或者定期删除之后,程序会向 AOF 文件追加(append)一条 DEL 命令,来显式地记录该键已被删除。

例如,如果客户端使用 GET message 命令,试图访问过期的 message 键,那么服务器将执行以下三个动作:

  • 从数据库中删除 message 键。
  • 追加一条 DEL message 命令到 AOF 文件。
  • 向执行 GET 命令的客户端返回空回复。

AOF 文件重写

和生成 RDB 文件时类似,在执行 AOF 重写的过程中,程序会对数据库中的键进行检查,已过期的键不会被保存到重写后的 AOF 文件中。

例如,如果数据库中包含三个键 k1、k2、k3,并且 k2 已经过期,那么在进行重写工作时,程序只会对 k1 和 k3 进行重写,而 k2 则会被忽略。因此,数据库中包含过期键不会对 AOF 重写造成影响。

Redis 复制

从上面惰性删除和定期删除的源码阅读中,我们可以发现,从库对于主库的过期键是不能主动进行删除的。如果一个主库创建的过期键值对,已经过期了,主库在进行定期删除的时候,没有及时的删除掉,这时候从库请求了这个键值对,当执行惰性删除的时候,因为是主库创建的键值对,这时候是不能在从库中删除的,那么是不是就意味着从库会读取到已经过期的数据呢?

当服务器运行在复制模式下时,从服务器的过期键删除动作由主服务器控制。从节点不会主动删除过期键,从节点会等待主节点触发键过期。当主节点触发键过期时,主节点会同步一个 del 命令给所有的从节点。因为是主节点驱动删除的,所以从节点会获取到已经过期的键值对。从节点需要根据自己本地的逻辑时钟来判断 Key 是否过期,从而实现数据集合的一致性读操作。

我们知道 Redis 中的过期策略是惰性删除和定期删除,所以每个键值的操作,都会使用惰性删除来检查是否过期,然后判断是否可以进行删除:

上面的惰性删除,对于主节点创建的过期 key ,虽然不能进行删除的操作,但是可以进行过期时间的判断,所以如果主库创建的过期键,如果主库没有及时进行删除,这时候从库可以通过惰性删除来判断键值对的是否过期,避免读取到过期的内容。

五、遗留问题

Redis 对缓存失效的处理机制大概分为两种,一种是客户端访问 key 的时候消极的处理,一种是主线程定期的积极地去执行缓存失效清理逻辑,上面文章对于一些细节还没有展开介绍,但是对于 Redis 缓存失效实现机制这个话题,本文留下几个问题:

  1. Redis 缓存失效逻辑为什么只有 master 才能操作?
  2. 上面提到如果客户端访问的是 slave,slave 并不会清理失效缓存,那么这次客户端岂不是获取了失效的缓存?
  3. 上面介绍的两种缓存失效机制各有什么优缺点?Redis 设计者为什么这么设计?
  4. 服务端对客户端的请求处理是单线程的,单线程又要去处理失效的缓存,是不是会影响 Redis 本身的服务能力?

Redis 的内存淘汰策略的选取并不会影响过期的 key 的处理。内存淘汰策略用于处理内存不足时的需要申请额外空间的数据;过期策略用于处理过期的缓存数据。

<参考>

https://www.jianshu.com/p/7badc549f316

https://mp.weixin.qq.com/s/XzwhwTVipu6aBQnvZoS1Ew


如果您觉得本站对你有帮助,那么可以支付宝扫码捐助以帮助本站更好地发展,在此谢过。
喜欢 (8)
[资助本站您就扫码 谢谢]
分享 (0)

您必须 登录 才能发表评论!