写点什么

一文详解 Redis 键过期策略

  • 2022 年 3 月 16 日
  • 本文字数:4231 字

    阅读完需:约 14 分钟

本文分享自华为云社区《Redis键过期策略详解》,作者:JavaEdge。

1 设置带过期时间的 key

# 时间复杂度:O(1),最常用方式expire key seconds
# 字符串独有方式setex(String key, int seconds, String value)
复制代码

除了 string 独有设置过期时间的方法,其他类型都需依靠 expire 方法设置时间,若:

  • 未设置时间,则缓存永不过期

  • 设置过期时间,但之后又想让缓存永不过期,使用 persist



设置key的过期时间。超时后,将会自动删除该key。在 Redis 的术语中一个key的相关超时是 volatile 的。

生存时间可以通过使用 DEL 命令来删除整个 key 来移除,或者被 SET 和 GETSET 命令覆写(overwrite)。这意味着,如果一个命令只是修改(alter)一个带生存时间的 key 的值而不是用一个新的 key 值来代替(replace)它的话,那么生存时间不会被改变。 如使用 INCR 递增 key 的值,执行 LPUSH 将新值推到 list 中或用 HSET 改变 hash 的field,这些操作都使超时保持不变。


  • 使用 PERSIST 命令可以清除超时,使其变成一个永久key

  • 若 key 被 RENAME 命令修改,相关的超时时间会转移到新key

  • 若 key 被 RENAME 命令修改,比如原来就存在 Key_A,然后调用 RENAME Key_B Key_A 命令,这时不管原来 Key_A 是永久的还是设为超时的,都会由Key_B的有效期状态覆盖


注意,使用非正超时调用 EXPIRE/PEXPIRE 或具有过去时间的 EXPIREAT/PEXPIREAT 将导致 key 被删除而不是过期(因此,发出的 key 事件将是 del,而不是过期)。

1.1 刷新过期时间

对已经有过期时间的key执行EXPIRE操作,将会更新它的过期时间。有很多应用有这种业务场景,例如记录会话的 session。

1.2 Redis 之前的 2.1.3 的差异

在 Redis 版本之前 2.1.3 中,使用更改其值的命令更改具有过期集的密钥具有完全删除 key 的效果。由于现在修复的复制层中存在限制,因此需要此语义。


EXPIRE 将返回 0,并且不会更改具有超时集的键的超时。

1.3 返回值

  • 1,如果成功设置过期时间。

  • 0,如果key不存在或者不能设置过期时间。

1.4 示例



假设有一 Web 服务,对用户最近访问的最新 N 页感兴趣,这样每个相邻页面视图在上一个页面之后不超过 60 秒。从概念上讲,可以将这组页面视图视为用户的导航会话,该会话可能包含有关 ta 当前正在寻找的产品的有趣信息,以便你可以推荐相关产品。


可使用以下策略轻松在 Redis 中对此模式建模:每次用户执行页面视图时,您都会调用以下命令:

MULTIRPUSH pagewviews.user:<userid> http://.....EXPIRE pagewviews.user:<userid> 60EXEC
复制代码

如果用户空闲超过 60 秒,则将删除该 key,并且仅记录差异小于 60 秒的后续页面视图。此模式很容易修改,使用 INCR 而不是使用 RPUSH 的列表。

1.5 带过期时间的 key

通常,创建 Redis 键时没有关联的存活时间。key 将永存,除非用户以显式方式(例如 DEL 命令)将其删除。EXPIRE 族的命令能够将过期项与给定 key 关联,但代价是该 key 使用的额外内存。当 key 具有过期集时,Redis 将确保在经过指定时间时删除该 key。可使用 EXPIRE 和 PERSIST 命令(或其他严格命令)更新或完全删除生存的关键时间。

1.6 过期精度

在 Redis 2.4 中,过期可能不准确,并且可能介于 0 到 1 秒之间。由于 Redis 2.6,过期误差从 0 到 1 毫秒。

1.7 过期和持久化

过期信息的键存储为绝对 Unix 时间戳(Redis 版本 2.6 或更高版本为毫秒)。这意味着即使 Redis 实例不处于活动状态,时间也在流动。要使过期工作良好,必须稳定计算机时间。若将 RDB 文件从两台计算机上移动,其时钟中具有大 desync,则可能会发生有趣的事情(如加载时加载到过期的所有 key)。即使运行时的实例,也始终会检查计算机时钟,例如,如果将一个 key 设置为 1000 秒,然后在将来设置计算机时间 2000 秒,则该 key 将立即过期,而不是持续 1000 秒。

2 Redis 的 key 过期策略

  • 被动方式 - 惰性删除

  • 主动方式 - 定期删除


为保证 Redis 的高性能,所以不会单独安排一个线程专门去删除。

2.1 惰性删除

key 过期时不删除,每次获取 key 时,再去检查是否过期。若过期,则删除,返回 null。

2.1.1 优点

删除操作只发生在取 key 时,且只删除当前 key,所以对 CPU 时间占用较少。此时删除已非做不可,毕竟若还不删除,就会获取到已过期 key。


当查询该 key 时,Redis 再很懒惰地检查是否删除。这和 Spring 的延迟初始化有着异曲同工之妙。

2.1.2 缺点

但这是不够的,因为有过期 key,永远不会再访问。若大量 key 在超出 TTL 后,很久一段时间内,都没有被获取过,则可能发生内存泄露(无用垃圾占用了大量内存)。


无论如何,这些 key 都应过期,因此还需要定期 Redis 在具有过期集的 key 之间随机测试几个 key。已过期的所有 key 将从 key 空间中删除。

定时删除

在设置 key 的过期时间的同时,为该 key 创建一个定时器,让定时器在 key 的过期时间来临时,对 key 进行删除。

优点

保证内存被尽快释放

缺点

  • 若过期 key 很多,删除这些 key 会占用很多的 CPU 时间,在 CPU 时间紧张的情况下,CPU 不能把所有的时间用来做要紧的事儿,还需要去花时间删除这些 key

  • 定时器的创建耗时,若为每一个设置过期时间的 key 创建一个定时器(将会有大量的定时器产生),性能影响严重

所以没人用

2.2 定期删除

每隔一段时间执行一次删除过期 key 操作。

优点

  • 通过限制删除操作的时长和频率,来减少删除操作对 CPU 时间的占用–处理"定时删除"的缺点

  • 定期删除过期 key–处理"惰性删除"的缺点

缺点

  • 在内存友好方面,不如"定时删除"

  • 在 CPU 时间友好方面,不如"惰性删除"

难点

  • 合理设置删除操作的执行时长(每次删除执行多长时间)和执行频率(每隔多长时间做一次删除)(这个要根据服务器运行情况来定了)


每秒 10 次:

  1. 测试 20 个带有过期的随机键

  2. 删除找到的所有已过期 key

  3. 如果超过 25% 的 key 已过期,从步骤 1 重新开始


这是个微不足道的概率算法,假设样本为整个 key 空间,继续过期,直到可能过期的 key 百分比低于 25%。这意味着在任何给定时刻,使用内存的已过期的最大键量等于最大写入操作量/秒除以 4。

定期删除流程



void databasesCron(void) {    /* Expire keys by random sampling. Not required for slaves     * as master will synthesize DELs for us. */    if (server.active_expire_enabled) {        if (iAmMaster()) {            activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW);        } else {            expireSlaveKeys();        }    }
复制代码


#define ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP 20 /* Keys for each DB loop. */
void activeExpireCycle(int type) { config_keys_per_loop = ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP + ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP/4*effort, // step1 for (j = 0; j < dbs_per_call && timelimit_exit == 0; j++) { /* 如果没有什么可以过期,请尽快尝试下一个数据库 */ // step2 if ((num = dictSize(db->expires)) == 0) { db->avg_ttl = 0; // step3 break; } }}
复制代码

对指定 N 个库的每个库随机删除≤指定个数的过期 K。

  1. 遍历每个数据库(redis.conf 中配置的"database"数量,默认 16)

  2. 检查当前库中的指定个数的 key(默认每个库检查 20 个 key,这相当于该循环执行 20 次,循环体为下边的描述)

  3. 若当前库没有一个 K 设置 TTL,直接执行下一个库的遍历

  4. 随机获取一个设置 TTL 的 K,检查其是否过期,若过期,则删除

  5. 判断定期删除操作是否已达到指定时长,若已达到,直接退出定期删除


定期删除程序中有个全局变量 current_db,记录下一个将要遍历的库。默认 16 个库,这次定期删除遍历了 10 个,那此时 current_db 就是 11,下一次定期删除就从第 11 个库开始遍历,假设 current_db 等于 15,那之后就再从 0 号库开始遍历(此时 current_db==0)

Redis 采用的过期策略

惰性删除+定期删除。

惰性删除流程

在进行 get 或 setnx 等操作时,先检查 key 是否过期:

  • 若过期,删除 key,然后执行相应操作

  • 若没过期,直接执行相应操作

RDB 处理过期 key

过期 key 对 RDB 无影响:

  • 从内存数据库持久化数据到 RDB 文件

    持久化 key 之前,会检查是否过期,过期的 key 不进入 RDB 文件

  • 从 RDB 文件恢复数据到内存数据库

    数据载入数据库之前,会对 K 进行过期检查,若过期,不导入数据库(主库情况)

AOF 处理过期 K

过期 key 对 AOF 没有任何影响。

从内存数据库持久化到 AOF 文件

  • 当 key 过期后,还没有被删除,此时进行执行持久化操作(该 key 不会进入 aof 文件,因为没有发生修改命令)

  • 当 key 过期后,在发生删除操作时,程序会向 aof 文件追加一条 del 命令(在将来的以 aof 文件恢复数据的时候该过期的键就会被删掉)

AOF 重写

重写时,会先判断 key 是否过期,已过期的 key 不会重写到 aof 文件

2.3 在复制链路和 AOF 文件中处理过期的方式

为了在不牺牲一致性的情况下获得正确行为,当 key 过期时,DEL 操作将同时在 AOF 文件中合成并获取所有附加的从节点。这样,过期的这个处理过程集中到主节点中,还没有一致性错误的可能性。


但是,虽然连接到主节点的从节点不会独立过期 key(但会等待来自 master 的 DEL),但它们仍将使用数据集中现有过期的完整状态,因此,当选择 slave 作为 master 时,它将能够独立过期 key,完全充当 master。


默认每台 Redis 服务器有 16 个数据库,默认使用 0 号数据库,所有的操作都是对 0 号数据库的操作

# 设置数据库数量。默认为16个库,默认使用DB 0,可使用"select 1"来选择一号数据库# 注意:由于默认使用0号数据库,那么我们所做的所有的缓存操作都存在0号数据库上,# 当你在1号数据库上去查找的时候,就查不到之前set过的缓存# 若想将0号数据库上的缓存移动到1号数据库,可以使用"move key 1"databases 16
复制代码
  • memcached 只是用了惰性删除,而 redis 同时使用了惰性删除与定期删除,这也是二者的一个不同点(可以看做是 redis 优于 memcached 的一点)

  • 对于惰性删除而言,并不是只有获取 key 的时候才会检查 key 是否过期,在某些设置 key 的方法上也会检查(eg.setnx key2 value2:该方法类似于 memcached 的 add 方法,如果设置的 key2 已经存在,那么该方法返回 false,什么都不做;如果设置的 key2 不存在,那么该方法设置缓存 key2-value2。假设调用此方法的时候,发现 redis 中已经存在了 key2,但是该 key2 已经过期了,如果此时不执行删除操作的话,setnx 方法将会直接返回 false,也就是说此时并没有重新设置 key2-value2 成功,所以对于一定要在 setnx 执行之前,对 key2 进行过期检查)


可是,很多过期 key,你没及时去查,定期删除也漏掉了,大量过期 key 堆积内存,Redis 内存殆耗尽!因此内存满时,还需有内存淘汰机制!这就是 Redis 自己 主动删除 数据了!


点击关注,第一时间了解华为云新鲜技术~​

发布于: 刚刚阅读数: 2
用户头像

提供全面深入的云计算技术干货 2020.07.14 加入

华为云开发者社区,提供全面深入的云计算前景分析、丰富的技术干货、程序样例,分享华为云前沿资讯动态,方便开发者快速成长与发展,欢迎提问、互动,多方位了解云计算! 传送门:https://bbs.huaweicloud.com/

评论

发布
暂无评论
一文详解Redis键过期策略_redis_华为云开发者社区_InfoQ写作平台