Redis 过期删除和内存淘汰

Redis 过期删除和内存淘汰

关键词:定时删除惰性删除定期删除LRULFU

Redis 过期删除

Redis 可以为每个键设置过期时间,当键过期时,会自动删除该键。

设置键的生存时间或过期时间

Redis 中,和键的生存时间相关的命令如下所示:

命令 描述
EXPIRE 设置 key 的过期时间,单位为秒
PEXPIRE 设置 key 的过期时间,单位为毫秒
EXPIREAT 设置 key 的过期时间为指定的秒级时间戳
PEXPIREAT 设置 key 的过期时间为指定的毫秒级时间戳
TTL 返回 key 的剩余生存时间,单位为秒
PTTL 返回 key 的剩余生存时间,单位为毫秒
PERSIST 移除 key 的过期时间,key 将持久保持

【示例】EXPIRE、TTL 操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
> set key value
OK
# 设置 key 的生存时间为 60s
> expire key 60
(integer) 1
# 查看 key 的剩余生存时间
> ttl key
(integer) 58
# 60s 之内
> get key
"value"
# 60s 之外
> get key
(nil)

【示例】EXPIREAT、TTL 操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
> set key value
OK
# 设置 key 的生存时间为 1692419299
> expireat key 1692419299
(integer) 1
# 查看 key 的剩余生存时间
> ttl key
(integer) 9948
# 1692419299 之前
> get key
"value"
# 1692419299 之后
> get key
(nil)

如何保存过期时间

在 Redis 中,redisDb 结构的 expires 字典保存了数据库中所有键的过期时间,这个字典称为过期字典:

  • 过期字典的键是一个指针,这个指针指向某个键对象
  • 过期字典的值是一个 long long 类型的整数,这个整数保存了键的过期时间——一个毫秒精度的 UNIX 时间戳。
1
2
3
4
5
6
7
8
9
10
typedef struct redisDb {

// 数据库键空间,保存着数据库中的所有键值对
dict *dict;

// 键的过期时间,字典的键为键,字典的值为过期事件 UNIX 时间戳
dict *expires;

// ...
} redisDb;

下图是一个带有过期字典的示例:

当执行 EXPIREPEXPIREEXPIREATPEXPIREAT 命令,Redis 都会将其转为 PEXPIREAT 形式的时间戳,然后维护在 expires 字典中。

过期键的判定

过期键的判定流程如下:

  • 检查指定 key 是否存在于过期字典;如果存在,则取得 key 的过期时间。
  • 检查当前时间戳是否大于 key 的过期时间:如果是,key 已过期;反之,key 未过期。

过期删除策略

  • 定时删除 - 在设置 key 的过期时间的同时,创建一个定时器,让定时器在 key 的过期时间来临时,立即执行对 key 的删除操作。
    • 优点 - 保证过期 key 被尽可能快的删除,释放内存。
    • 缺点 - 如果过期 key 较多,可能会占用相当一部分的 CPU,从而影响服务器的吞吐量和响应时延
  • 惰性删除 - 放任 key 过期不管,但是每次访问 key 时,都检查 key 是否过期,如果过期的话,就删除该 key ;如果没有过期,就返回该 key。
    • 优点 - 占用 CPU 最少。程序只会在读写键时,对当前键进行过期检查,因此不会有额外的 CPU 开销。
    • 缺点 - 过期的 key 可能因为没有被访问,而一直无法释放,造成内存的浪费,有内存泄漏的风险
  • 定期删除 - 每隔一段时间,程序就对数据库进行一次检查,删除里面的过期 key 。至于要删除多少过期 key ,以及要检查多少个数据库,则由算法决定。定期删除是前两种策略的一种折中方案。定期删除策略的难点是删除操作执行的时长和频率。
    • 执行太频或执行时间过长,就会出现和定时删除相同的问题;
    • 执行太少或执行时间过短,就会出现和惰性删除相同的问题;

Redis 的过期删除策略

Redis 同时采用了惰性删除和定期删除策略,以此在合理使用 CPU 和内存之间取得平衡。

Redis 定期删除策略的实现 - 由 redis.c/activeExpireCycle 函数实现,每当 Redis 周期性执行 redis.c/serverCron 函数时,activeExpireCycle 函数就会被调用。activeExpireCycle 函数会在规定时间内,遍历各个数据库,从 expires 字典中随机检查一部分键的过期时间,并删除过期的键。

Redis 惰性删除策略的实现 - 由 db.c/expireIfNeeded 函数实现,所有读写命令在执行之前都会调用 expireIfNeeded 函数对输入键进行检查:如果输入键已过期,将输入键从数据库中删除;否则,什么也不做。

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

  • 生成 RDB 文件 - 执行 SAVE 命令或者 BGSAVE 命令,所产生的新 RDB 文件“不会包含已经过期的键”
  • 载入 RDB 文件 - 主服务器“不会载入已过期的键”从服务器会载入“会载入已过期的键”
  • 生成 AOF 文件 - 当一个过期键未被删除时,不会影响 AOF 文件;当一个过期键被删除之后, 服务器会追加一条 DEL 命令到现有 AOF 文件的末尾, 显式地删除过期键。
  • 重写 AOF 文件 - 执行 BGREWRITEAOF 命令所产生的重写 AOF 文件“不会包含已经过期的键”
  • 复制 - 当主服务器删除一个过期键之后, 它会向所有从服务器发送一条 DEL 命令, 显式地删除过期键。从服务器即使发现过期键, 也不会自作主张地删除它, 而是等待主节点发来 DEL 命令, 这种统一、中心化的过期键删除策略可以保证主从服务器数据的一致性。
  • 当 Redis 命令对数据库进行修改之后, 服务器会根据配置, 向客户端发送数据库通知。

Redis 内存淘汰

Redis 内存淘汰要点

  • 失效时间 - 作为一种定期清理无效数据的重要机制,在 Redis 提供的诸多命令中,EXPIREEXPIREATPEXPIREPEXPIREAT 以及 SETEXPSETEX 均可以用来设置一条键值对的失效时间。而一条键值对一旦被关联了失效时间就会在到期后自动删除(或者说变得无法访问更为准确)。
  • 最大缓存 - Redis 允许通过 maxmemory 参数来设置内存最大值。当内存达设定的阀值,就会触发内存淘汰
  • 内存淘汰 - 内存淘汰是为了更好的利用内存——清理部分缓存,以此换取内存的利用率,即尽量保证 Redis 缓存中存储的是热点数据。
  • 非精准的 LRU - 实际上 Redis 实现的 LRU 并不是可靠的 LRU,也就是名义上我们使用 LRU 算法淘汰键,但是实际上被淘汰的键并不一定是真正的最久没用的。

Redis 内存淘汰策略

内存淘汰只是 Redis 提供的一个功能,为了更好地实现这个功能,必须为不同的应用场景提供不同的策略,内存淘汰策略讲的是为实现内存淘汰我们具体怎么做,要解决的问题包括淘汰键空间如何选择?在键空间中淘汰键如何选择?

Redis 提供了下面几种内存淘汰策略供用户选:

  • 不淘汰
    • noeviction - 当内存使用达到阈值的时候,所有引起申请内存的命令会报错。这是 Redis 默认的策略。
  • 在过期键中进行淘汰
    • volatile-random - 在设置了过期时间的键空间中,随机移除某个 key。
    • volatile-ttl - 在设置了过期时间的键空间中,具有更早过期时间的 key 优先移除。
    • volatile-lru - 在设置了过期时间的键空间中,优先移除最近未使用的 key。
    • volatile-lfu (Redis 4.0 新增)- 淘汰所有设置了过期时间的键值中,最少使用的键值。
  • 在所有键中进行淘汰
    • allkeys-lru - 在主键空间中,优先移除最近未使用的 key。
    • allkeys-random - 在主键空间中,随机移除某个 key。
    • allkeys-lfu (Redis 4.0 新增) - 淘汰整个键值中最少使用的键值。

如何选择淘汰策略

  • 如果数据呈现幂等分布,也就是一部分数据访问频率高,一部分数据访问频率低,则使用 allkeys-lruallkeys-lfu
  • 如果数据呈现平均分布,也就是所有的数据访问频率都相同,则使用 allkeys-random
  • 若 Redis 既用于缓存,也用于持久化存储时,适用 volatile-lruvolatile-lfuvolatile-random。但是,这种情况下,也可以部署两个 Redis 集群来达到同样目的。
  • 为 key 设置过期时间实际上会消耗更多的内存。因此,如果条件允许,建议使用 allkeys-lruallkeys-lfu,从而更高效的使用内存。

参考资料