Redis系列(五):Redis 缓存机制与淘汰策略


Redis 是基于内存的 Key Value 的 NoSql 数据库,由于其高性能,高可用,支持分布式集群的优点被广泛应用于缓存的业务场景。本篇文章就来详细了解下 Redis 缓存机制及内存淘汰策略。

如何使用缓存?

我们先来插入一个最简单的 key

1
2
3
127.0.0.1:6379> set name phachon
OK
127.0.0.1:6379>

OK, 插入成功。我们再来设置一下 key 的过期时间, redis 有 4 个命令来设置过期时间:

  • EXPIRE :key 的生存时间设置为 ttl 秒
  • PEXPIRE :key 的生存时间设置为 ttl 毫秒
  • EXPIREAT :将 key 的过期时间设置为 timestamp 指定的秒数时间戳
  • PEXPIREAT :将 key 的过期时间设置为 timestamp 指定的毫秒数时间戳
1
2
3
4
5
6
7
8
127.0.0.1:6379> EXPIRE name 1000
(integer) 1
127.0.0.1:6379> PEXPIRE name 30000
(integer) 1
127.0.0.1:6379> EXPIREAT name 1539331476
(integer) 1
127.0.0.1:6379> PEXPIREAT name 1539337307000
(integer) 1

OK, 该 key 在 1000s 之后将过期。过期之前我们可以通过命令查看剩余的时间

1
2
3
127.0.0.1:6379> ttl name
(integer) 936
127.0.0.1:6379>

可以看到,剩余 936s 该 key 才会过期,ttl 返回的是秒,如果想要看剩余多少毫秒,可以使用 pttl 命令

有的时候,我们可以将 set 和 expire 命令合并一个命令使用

1
2
3
127.0.0.1:6379> setex name 1000 phachon
OK
127.0.0.1:6379>

注意:setex 命令只能对字符串类型的数据进行 setexpire 操作。

看来使用缓存的方法是非常的简单。这里只演示了字符串类型的数据,其他的数据类型也是用的 expire 命令来设置过期时间。

如何判断过期?

RedisDb 结构的 expires 字典保存了所有 key 的过期时间。

1
那么 Redis 是使用什么方法来判断时间是否过期并删除呢?

在了解 Redis 的删除过期键的策略之前我们来看看有哪些方式可以实现:

  • 定时删除
    定时删除恐怕是我们最容易想到的方法了,原理就是在一个 key 被设置了过期时间之后,启动一个定时器,当定时器到了过期时间,则删除掉这个 key。
    该种方法肯定能保证键的过期删除,并且不会有遗漏的键,但是要为每一个key实现一个定时器,会耗费较多的资源,无疑会对 CPU 和当前任务造成影响。

  • 定期删除
    定期删除策略的原理就是规定一定时间内定期的扫描一遍 expires 字典,将已过期的键删除掉。这种方法解决了定时删除的耗费资源的问题,但是该种方法不能保证所有的 key 过期删除。
    例如:key1 生存时间是 3s, 而定期删除的时间间隔是 5s, 那这个 key 在 3 秒后还存在内存中,并没有被删除。

  • 惰性删除
    惰性删除的原理是,在获取每一个 key 的时候,判断一下该 key 是否已经过了过期时间,如果已经失效,则删除掉这个 key。惰性删除的缺点也很明显,如果一个 key 一直不使用,则即使到了过
    期时间也会一直占有内存,大量的不使用的 key 会使得内存暴增。

综合三种过期键删除的策略我们发现似乎都不能很好的完成过期键的精确删除。Redis 采用了两种删除策略来协同工作: 定期删除 + 惰性删除

  1. 使用定期删除策略:Redis 会每隔一段时间(默认是 100 ms)会从所有的 key 中随机获取一些 key ,判断时间是否过期,是,则删除。
    1
    为什么不直接获取整个数据,而只随机找到一些 key 来判断,这样不是还有可能会造成某些 key 一直未被判断?

如果每隔 100ms 检查所有的 key, 如果数据库有 1000 万缓存key,那 redis 岂不是卡死。

采用定期删除之后,还是会导致很多的已过期的缓存 key 没有被删除。

  1. 使用惰性删除策略:Redis 会在每次 get key 的时候先判断该 key 的过期时间,已经过期,则删除。
1
采用了 定期删除 + 惰性删除 的策略,假如有一个 key , 即没有被定期删除随机获取到,也没有被使用,那么这个 key 岂不是永远还占用着内存?

没错,即使采用了 定期删除+惰性删除 的策略,还是不能保证所有的过期 key 都被删除。这种情况下,还是会导致内存占用率增高。

有解决办法吗,有!那就是启用 内存淘汰策略

内存淘汰策略

内存淘汰策略:Redis 每执行一个命令,就会判断当前占用的内存是否大于设置的最大内存,大于,则开始内存淘汰机制。

1
2
# 设置最大的内存,如果不设置该值的话,会导致 redis 一直运行最终以内存不足而终止
# maxmemory <bytes>

Redis 内存淘汰的机制有以下几种方案可供选择:

  • volatile-lru:从设置过期的数据集中淘汰最少使用的 key
  • volatile-ttl:从设置过期的数据集中淘汰即将过期的 key
  • volatile-random:从设置过期的数据集中随机选取 key 淘汰
  • allkeys-lru:从所有的数据集中选取最少使用的数据
  • allkyes-random:从所有的数据集中任意选取数据淘汰
  • no-envicition:不进行淘汰

注意这 6 主机制。volatile 和 allkeys 规定了是对已设置过期的数据集还是从全部的数据集淘汰数据。具
体的使用应根据不同的业务场景来配置不同的策略。一般来说,使用 volatile-lru 或者 allkeys-lru 是比
较合理的。删除最少使用的 key。如果使用 redis 作为缓存,就使用 volatile-lru 。如果除了缓存还使用存储,就使用 allkeys-lru。

注意:ttl 和 random 的实现方法比较简单,lru 的实现方法是 redis 会随机挑选几个键值对,然后取出
lru 最大的键值对淘汰。所以并不是严格的就会淘汰整个数据集中最少使用的 key,而是随机数据集中的最少使用的 key。

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

1
那么 AOF、RDB持久化和复制功能对过期策略会有什么影响呢?

AOF、RDB持久化和主从复制的实现原理这里不再详细介绍,可参考之前写的文章。

RDB 文件的生成和载入

当执行 SAVE 或者是 BGSAVE 命令的时候,程序会对数据库中所有的键进行检查,已过期的键不会被
保存到新创建的 RDB 文件中。 所以数据库保存过期键不会对 RDB 文件的创建产生影响。

当服务启动的时候,如果开启了 快照持久化 则会载入 rdb 文件,如果是主从同步模式的集群模式,不同的节点的处理不一样:

  • 如果启动的服务节点是 Master 节点,则程序会对 rdb 文件中的键检查,只会将未过期的键载入到内存中。过期的键会忽略。
  • 如果启动的服务节点是 Slave 节点:则程序不会对 rdb 文件中键检查,不论是否过期都会载入到
    数据库中。因为主从同步模式,当从节点重启之后,会再次和主节点同步,所以,最后数据会和主节点保持一致。过期的 key 依然会被删除。

AOF 文件的写入

当服务时开启了 AOF 的持久化机制时,如果某个 key 已过期,但是还是没有被 定期删除惰性删除 清理掉,程序不会对 aof 文件做任何操作。
当过期的 key 被 定期删除惰性删除 删除之后,程序会向 aof 文件写一条 DEL 命令来记录该键被删除。举例说明:
客户端使用 GET message 命令,试图访问过期的 key,那么程序执行以下步骤:

  1. 从数据库中删除 message 键
  2. 在 aof 文件追加一条 DEL 命令
  3. 向客户端返回空

AOF 重写日志

如果开启了 AOF 重写机制,再重写的子进程开始时,程序会对数据库中的 key 检查,并且只会将未过期的 key 写入到 aof 临时文件中。

主从复制

在处于主从复制的模式中,如果主服务器的过期键要删除:

  • 主服务器在删除了自己的数据之后,会发送一个 DEL 的命令给从服务器,告知从服务器这个键要删除
  • 从服务器只有在接受到这个 DEL 命令才会真正的将数据删除,即使从服务器在执行自己的 定期删除惰性删除时,也不会删除该数据。
  • 从服务只会在接受到主服务器的 DEL 命令才会删除过期键

主从复制规定数据的过期删除完全由主节点服务器来控制,正是通过这种机制,才能保证主从服务器的数据一致性。

总结

通过上面过期机制的了解,我们发现 Redis 并不能保证所有的 key 都能准时过期并删除。所以通过多种机制来协作保证。主要是采用:
定期删除 + 惰性删除 + 内存淘汰策略

参考

《Redis 设计与实现》
Redis的缓存策略和主键失效机制