Redis 过期删除和内存淘汰
Redis 内存管理
关键词:
定时删除
、惰性删除
、定期删除
、LRU
、LFU
Redis 过期删除
Redis 可以为每个键设置过期时间,当键过期时,会自动删除该键。
设置键的生存时间或过期时间
Redis 中,和键的生存时间相关的命令如下所示:
命令 | 描述 |
---|---|
EXPIRE | 设置 key 的过期时间,单位为秒 |
PEXPIRE | 设置 key 的过期时间,单位为毫秒 |
EXPIREAT | 设置 key 的过期时间为指定的秒级时间戳 |
PEXPIREAT | 设置 key 的过期时间为指定的毫秒级时间戳 |
TTL | 返回 key 的剩余生存时间,单位为秒 |
PTTL | 返回 key 的剩余生存时间,单位为毫秒 |
PERSIST | 移除 key 的过期时间,key 将持久保持 |
【示例】EXPIRE、TTL 操作
> 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 操作
> 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 时间戳。
typedef struct redisDb {
// 数据库键空间,保存着数据库中的所有键值对
dict *dict;
// 键的过期时间,字典的键为键,字典的值为过期事件 UNIX 时间戳
dict *expires;
// ...
} redisDb;
下图是一个带有过期字典的示例:
当执行 EXPIRE
、PEXPIRE
、EXPIREAT
、PEXPIREAT
命令,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 提供的诸多命令中,
EXPIRE
、EXPIREAT
、PEXPIRE
、PEXPIREAT
以及SETEX
和PSETEX
均可以用来设置一条键值对的失效时间。而一条键值对一旦被关联了失效时间就会在到期后自动删除(或者说变得无法访问更为准确)。 - 最大缓存 - 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-lru
或allkeys-lfu
。 - 如果数据呈现平均分布,也就是所有的数据访问频率都相同,则使用
allkeys-random
。 - 若 Redis 既用于缓存,也用于持久化存储时,适用
volatile-lru
、volatile-lfu
、volatile-random
。但是,这种情况下,也可以部署两个 Redis 集群来达到同样目的。 - 为 key 设置过期时间实际上会消耗更多的内存。因此,如果条件允许,建议使用
allkeys-lru
或allkeys-lfu
,从而更高效的使用内存。