简介

Redis 是使用非常广泛的 Key-Value 内存数据库。因为数据都存放在内存中,所以存取速度非常快。不过,很多情况下我们需要将 Redis 中的数据保存到硬盘中以便做备份。Redis 提供了两种数据持久化方式,分别是 RDB 和 AOP,本文分析这两种方式的使用以及过期键对持久化的影响。

RDB

RDB 指的是将 Redis 数据库在某个时间点的快照保存到磁盘,所生成的 RDB 文件是一个经过压缩的二进制文件,通过这个文件可以还原出 Redis 的数据状态。

创建快照的方式有以下几种:

  • 客户端向 Redis 发送 ​​BGSAVE​​ 命令,Redis 会调用 fork 创建一个子进程,然后子进程负责将快照写入硬盘,而父进程继续处理命令请求。
  • 客户端向 Redis 发送 ​​SAVE​​ 命令,此时 Redis 将开始创建快照,并且在完成之前不再响应其它命令。
  • 用户设置 save 配置选项,比如 ​​save 60 10000​​,那么从 Redis 最近一次创建快照算起,当 “60 秒内有 10000 次写入” 这个条件被满足时, Redis 就会自动触发 ​​BGSAVE​​ 命令。如果用户设置了多个 save 配置选项,那么当任意一个 save 配置满足时,Redis 就会触发一次 ​​BGSAVE​​ 命令。save 配置的格式如下所示:

save 60 10000
stop-writes-on-bgsave-error no
rdbcompression yes // 使用压缩
dbfilename dump.rdb // RBD 文件的名字

dir ./


  • 当 Redis 通过 ​​SHUTDOWN​​ 命令接收到关闭服务器的请求时,或者接收到 TERM命令时,会执行一个 ​​SAVE​​ 命令,并且阻塞所有的客户端,不再执行任何请求。在 ​​SAVE​​ 命令执行结束后关闭服务器。
  • 当一个 Redis 服务器连接另一个 Redis 服务器,并向对方发送 ​​SYNC​​ 命令来开始一次复制操作的时候,如果主服务器没有在执行 ​​BGSAVE​​ 操作,或者主服务器并非刚执行完 ​​BGSAVE​​,那么主服务器会执行 ​​BGSAVE​​ 命令。

RDB 的主要问题是,如果系统发生崩溃,那么最近一次执行完快照后修改的数据将被丢失。因此,RDB 适合用于即使丢失一部分数据也不会造成影响的应用程序。

AOF

AOF 指的是将所有执行的写命令写到 AOF 文件的末尾,以此来记录数据发生的变化。如果 Redis 想要恢复 AOF 中的数据,只要重新执行一次 AOF 文件中所包含的写命令就可以。

AOF 的配置如下所示:


appendonly yes // 打开 aof
appendfsync everysec // aof 同步的频率
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100 // 文件大小增长比超过这个值开始自动重写 aof
auto-aof-rewrite-min-size 64mb // 文件大小超过这个值才可以有可能自动重写 aof


上面的配置中,​​appendfsync everysec​​ 设置的是同步的频率。应用程序在向硬盘写入数据的时候有 3 个步骤:

  1. 调用 ​​file.write()​​ 向文件写入,此时需要写入的内容被存储到了缓冲区,并不是真正写到硬盘上了。
  2. 操作系统在某个时候将缓冲区的内容写入硬盘,这时数据才真正被持久化了。

操作系统使用以上的文件写入方式是为了提高性能,毕竟硬盘 I/O 操作是比较耗时的。但是,这种方式的缺点在于如果机器崩溃了那么缓冲区的内容将丢失。程序可以使用 ​​file.flush()​​ 来请求操作系统尽快地将缓冲区的内容刷新到硬盘上,不过何时开始执行仍然由操作系统决定。程序也可以命令操作系统将文件同步 ( sync ) 到硬盘,同步操作会阻塞应用程序直到数据被写入硬盘。当同步操作完成后,即使系统出现故障,也不会对被同步的文件造成影响。

对于 Redis 来讲,可以指定 ​​appendfsync​​ 以何种方式让数据完全同步到硬盘,这个配置有 3 个选项:

  1. always: 每个 Redis 写命令都立即同步到硬盘,这是比较消耗性能的
  2. everysec: 每秒执行一次同步,兼顾性能与数据安全,是比较常用的选项
  3. no: 让操作系统决定何时进行同步

​always​​ 可以使得在 Redis 发生崩溃时丢失的数据最少,但是也是最消耗性能的,导致 Redis 的处理速度变慢。​​ererysec​​ 是一种兼顾性能与数据安全的方式,在这种情况下,如果系统崩溃,用户最多会丢失一秒内的数据。​​no​​ 选项完全将同步交给操作系统被决定,性能也不比 ​​everysec​​ 高多少,是不推荐的方式。

AOF 的缺点是随着 Redis 的不断运行,AOF 文件可能会非常大,甚至用完硬盘的空间。解决这个问题的办法是 AOF 重写。

重写

客户端可以发送 ​​BGREWRITEAOF​​ 命令让 Redis 重写 AOF 文件,Redis 会移除冗余的 AOF 命令进行重写,使得 AOF 文件的体积尽可能地小。

除了客户端主动发送 ​​BGREWRITEAOF​​ 命令,也可以使用配置让 Redis 在满足一定条件地情况下自动开始重写 AOF 文件。例如上一小节设置了 ​​auto-aof-rewrite-percentage 100​​ 和 ​​auto-aof-rewrite-min-size 64mb​​。这两个配置的含义是,如果 AOF 文件大于 64MB 并且比上一次重写之后的大小增加了一倍的时候,Redis 将执行 ​​BGREWERITEAOF​​ 命令。

过期键删除

用户往往为 Redis 中的键设定过期时间,因此需要一定的策略来删除过期键,可以有三种策略:

  1. 定时删除,即通过定时器在过期时间到达的时候删除过期的键。这种方式的优点是节省内存,不会因为大量的过期键占用内存资源,而缺点则是消耗 CPU 资源,尤其是过期键数量较多的时候,删除操作消耗太长时间,降低了 Redis 的响应时间。
  2. 惰性删除,即在每次获取某个键的时候判断是否过期,如果未过期,则正常返回其值,否则删除这个键,返回空。这种方式的优点是节省 CPU 资源,但是消耗了内存。尤其是过期键数量较多的时候,大量内存被无效的键占用,相当于内存泄露。
  3. 定期删除,即每隔一段时间周期对数据库中的键进行扫描,但是只扫描其中一部分,力求在内存和 CPU 之间达到一个平衡。

从上面 3 种策略可以看出,单用第一个肯定是不行的,Redis 的响应时间至关重要。第二个则是比较好的方式,在获取键的时候判断是否过期并决定是否删除,它的缺点是很多键无法及时删除。如果一个过期键再也没有被访问,那么它将永远留在内存中,而第三种方式正好可以弥补。

Redis 中过期键的删除策略正是惰性删除与定期删除的结合。

过期键与持久化

了解了过期键的删除策略后,下面看下键的过期时间对持久化的影响。

在生成 RDB 文件的过程中,如果一个键已经过期,那么其不会被保存到 RDB 文件中。在载入 RDB 的时候,要分两种情况:

  1. 如果 Redis 以主服务器的模式运行,那么会对 RDB 中的键进行时间检查,过期的键不会被恢复到 Redis 中。
  2. 如果 Redis 以从服务器的模式运行,那么 RDB 中所有的键都会被载入,忽略时间检查。在从服务器与主服务器进行数据同步的时候,从服务器的数据会先被清空,所以载入过期键不会有问题。

对于 AOF 来说,如果一个键过期了,那么不会立刻对 AOF 文件造成影响。因为 Redis 使用的是惰性删除和定期删除,只有这个键被删除了,才会往 AOF 文件中追加一条 DEL 命令。在重写 AOF 的过程中,程序会检查数据库中的键,已经过期的键不会被保存到 AOF 文件中。

在运行过程中,对于主从复制的 Redis,主服务器和从服务器对于过期键的处理也不相同:

  1. 对于主服务器,一个过期的键被删除了后,会向从服务器发送 DEL 命令,通知从服务器删除对应的键
  2. 从服务器接收到读取一个键的命令时,即使这个键已经过期,也不会删除,而是照常处理这个命令。
  3. 从服务器接收到主服务器的 DEL 命令后,才会删除对应的过期键。

这么做的主要目的是保证数据一致性,所以当一个过期键存在于主服务器时,也必然存在于从服务器。

总结

本文对 Redis 的两种持久化方式进行了简要的梳理,分析了 Redis 删除过期键的策略以及对持久化的影响。理解了这部分内容不仅可以让我们对 Redis 的使用更加得心应手,对于学习 Redis 的其它内容如复制的过程也会很有帮助。

参考

  • 《Redis 实战》
  • 《Redis 设计与实现》