一文搞懂Redis持久化方式RDB&AOF

362 阅读14分钟

前言

Redis的性能好的特性很大程度上是由于将所有数据都存储在了内存中,然而当 Redis 重启后,所有存储在内存中的数据就会丢失,在一些情况下,我们希望 Redis 在重启后能保证数据不丢失。我们可以让数据从内存中以某种形式同步到磁盘中,使得重启后可以根据硬盘中的记录恢复数据。这一过程就是持久化。

Redis支持两种方式的持久化,一种是 RDB ,一种是 AOF,前者会根据指定的规则“定时”将内存中的数据存储在磁盘上,而后者在每次执行命令后将命令本身记录下来,两种持久化方式可以单独使用其中一种,但更多情况是两者结合使用。

RDB

RDB方式的持久化是通过快照完成的,当符合一定条件时Redis会自动将内存中的所有数据生成一份副本并存储在磁盘上,这个过程即为“快照”,Redis会在以下几种情况下对数据进行快照:

  • 根据配置规则进行自动快照
  • SAVE 或者 BGSAVE 命令
  • 执行 flushall 命令
  • 执行复制时

载入 RDB 文件的实际工作由 rdb.c/rdbLoad 函数完成,这个函数和 rdbSave 函数之间的关系如下图所示。

数据快照的四种情况

1、根据配置规则进行自动快照

Redis允许用户自定义快照条件,当符合快照条件时,Redis会自动执行快照操作,进行快照的条件可以由用户在配置文件中自定义,由两个参数构成:时间窗口M 和 改动的键的个数N,每当时间M内被更改的键的个数大于N时,即符合自动快照条件。例如:

  • save 900 1
  • save 300 10
  • save 60 10000

可以同时存在多个条件,条件之间是“或”的关系,就这个例子,save 900 1 表示在 900s 内有一个或一个以上的键被更改则进行快照。

2、SAVE / BGSAVE 命令

除了让 Redis自动除了让Redis自动进行快照外,当进行服务重启、手动迁移以及备份时我们也会需要手动执行快照操作。Redis提供了两个命令来完成这一任务。

  1. SAVE命令

当执行SAVE命令时,Redis同步地进行快照操作,在快照执行的过程中会阻塞所有来自客户端的请求。当数据库中的数据比较多时,这一过程会导致Redis较长时间不响应,所以要尽量避免在生产环境中使用这一命令。

  1. BGSAVE命令

需要手动执行快照时推荐使用BGSAVE命令。BGSAVE命令可以在后台异步地进行快照操作,快照的同时服务器还可以继续响应来自客户端的请求。执行BGSAVERedis会立即返回OK表示开始执行快照操作,如果想知道快照是否完成,可以通过LASTSAVE命令获取最近一次成功执行快照的时间,返回结果是一个时间戳,如:

redis> LASTSAVE
( integer) 1423537869

因为 BGSAVE 命令的保存工作是由子进程执行的,所以在子进程创建RDB文件的过程中,Redis服务器仍然可以继续处理客户端的命令请求,但是,在 BGSAVE 命令执行期间,服务器处理 SAVEBGSAVEBGREWRITEAOF 这三个命令的方式会和平时有所不同。(其中,BGREWRITEAOF 命令是AOF重写命令,文章后面会进行介绍)

BGSAVE 命令执行期间,SAVE命令和 BGSAVE 命令都会被服务器拒绝,服务器禁止 SAVE命令和 BGSAVE 或者 两个 BGSAVE 命令同时执行是为了避免父进程(服务器进程)和子进程同时执行两个 rdbSave调用,防止产生竞争条件。

BGSAVE 命令执行期间也不能执行 BGREWRITEAOF 命令:

  • 如果 BGSAVE 命令正在执行,那么客户端发送的 BGREWRITEAOF 命令会被延迟到 BGSAVE命令执行完毕之后执行。
  • 如果 BGREWRITEAOF 命令正在执行,那么客户端发送的 BGSAVE 命令会被服务器拒绝

因为BGSAVEBGREWRITEAOF 命令的实际工作都由子进程执行,所以这两个命令在操作方面并没有什么冲突的地方,不能同时执行它们只是性能方面的考虑,这两个子进程都需要执行大量的磁盘写入操作,所以,要避免二者同时执行。

3、执行 FLUSHALL 命令

当执行FLUSHALL命令时,Redis 会清除数据库中的所有数据。需要注意的是,不论清空数据库的过程是否触发了自动快照条件,只要自动快照条件不为空,Redis就会执行一次快照操作。例如,当定义的快照条件为当 1 秒内修改 10000 个键时进行自动快照,而当数据库里只有一个键时,执行FLUSHALL命令也会触发快照,即使这一过程实际上只有一个键被修改了。

当没有定义自动快照条件时,执行 FLUSHALL 则不会进行快照。

4、执行复制时

当设置了主从模式时,Redis 会在复制初始化时进行自动快照。即使没有定义自动快照条件,并且没有手动执行过快照操作,也会生成 RDB快照文件。

RDB文件结构

image.png

1、RDB文件的开头是 REDIS 部分,长度为 5 字节,保存着 “REDIS”五个字符,通过五个字符,程序可以在载入文件时,快速检查所载入的文件是否是 RDB 文件。

2、db_version 长度为4字节,它的值是一个字符串标识的整数,这个整数记录着 RDB 文件的版本号,比如“0006”代表RDB文件版本为第六版

3、databases 包含着0个或任意多个数据库,以及各数据库中的键值对数据:

  • 如果服务器的数据库状态为空,那么这个部分也为空,长度为0字节
  • 如果服务器数据库状态为非空,那么这个部分也为非空,根据数据库所保存键值对的数量、类型和内容不同,这个部分的长度也会有所不同

4、EOF常量的长度为1字节,这个常量标志着RDB文件正文内容的结束,当读入程序遇到这个值时,它知道所有数据库的所以键值对都已经载入完毕了

5、check_num是8字节长的无符号整数,保存着一个校验和,这个校验和是程序通过对 REDISdb_versiondatabasesEOF四个部分的内容进行计算得出的,服务器在载入 RDB 文件时,会将载入数据所计算出的校验和与 check_sum所记录的校验和进行对比,以此来检查RDB文件是否有出错或损坏的情况出现。

AOF

除了 RDB 持久化功能外,Redis还支持了AOF(Append Only File)持久化功能,与 RDB 持久化通过保存数据库中的键值对来记录数据库状态不同,AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的。

服务器在启动时,可以通过载入和执行 AOF 文件中保存的命令来还原服务器关闭之前的数据库状态。

AOF持久化实现

AOF持久化功能的实现可以分为命令追加(append),文件写入,文件同步(sync)三个步骤。

命令追加

AOF 持久化功能处于打开状态时,服务器在执行完一个写命令后,会以协议格式将被执行的写命令追加到服务器状态的 aof_buf缓冲区的末尾。

文件写入与同步

Redis服务器进程是一个事件循环,这个循环中的文件事件负责接收客户端的命令请求,以及向客户端发送命令回复,而时间事件则负责执行像 serverCron 函数这样需要定时运行的函数。

为了提高文件的写入效率,在现代操作系统中,当用户调用write函数,将一些数据写入到文件的时候,操作系统通常会将写入数据暂时保存在一个内存缓冲区里面,等到缓冲区的空间被填满、或者超过了指定的时限之后,才真正地将缓冲区中的数据写入到磁盘里面。

这种做法虽然提高了效率,但也为写入数据带来了安全问题,因为如果计算机发生停机,那么保存在内存缓冲区里面的写入数据将会丢失。

为此,系统提供了fsyncfdatasync两个同步函数,它们可以强制让操作系统立即将缓冲区中的数据写入到硬盘里面,从而确保写入数据的安全性。

AOF文件的载入与数据还原

因为AOF文件里面包含了重建数据库状态所需的所有写命令,所以服务器只要读入并重新执行一遍AOF 文件里面保存的写命令,就可以还原服务器关闭之前的数据库状态。

Redis读取AOF文件并还原数据库状态的详细步骤如下:

  • 创建一个不带网络连接的伪客户端:因为 Redis 的命令只能在客户端上下文中执行,而载入AOF文件时使用的命令直接来源于AOF文件而不是网络连接,所以服务器使用了一个没有网络连接的伪客户端来执行AOF文件保存的写命令,伪客户端执行命令的效果和带网络连接的客户端执行命令的效果完全一样。
  • AOF文件中分析并读取出一条写命令
  • 使用伪客户端执行被读出的写命令
  • 一直执行步骤2和步骤3,直到AOF文件中的所有写命令都被处理完毕为止 当完成以上步骤之后,AOF文件所保存的数据库状态就会被完整地还原出来,整个过程如图所示。

AOF重写

因为AOF持久化是通过保存被执行的写命令来记录数据库状态的,所以随着服务器运行时间的流逝,AOF文件中的内容会越来越多,文件的体积也会越来越大,如果不加以控制的话,体积过大的AOF文件很可能对Redis服务器、甚至整个宿主计算机造成影响,并且AOF文件的体积越大,使用AOF文件来进行数据还原所需的时间就越多。

如果,对一个key进行了多次 set操作,那AOF文件中就要保存很多次set命令。为了解决AOF文件体积膨胀的问题,Redis提供了AOF文件重写功能,通过该功能,Redis服务器可以创建一个新的AOF文件代替现有的AOF文件,新旧两个AOF文件所保存的数据库状态相同,但新AOF文件不会包含任何浪费空间的冗余命令,所以新的AOF文件的体积通常比旧AOF文件要小很多。

实际上,AOF文件重写不需要对旧AOF文件进行读取、分析或者写入操作,这个功能是通过读取服务器当前的数据库状态来实现的。

例如服务器为了保存一个 list 键的状态,必须在AOF文件中写入很多set命令,如果想要用最少的命令记录当前状态,最简单高效的方法不是读取和分析现有的AOF文件,而是直接从数据库中读取此 list 的值,然后用一条 set 命令来代替保存AOF文件的很多命令。

上面介绍的AOF重写程序aof_rewrite函数可以很好地完成创建一个新AOF文件的任务,但是,因为这个函数会进行大量的写入操作,所以调用这个函数的线程将被长时间阻塞,因为Redis服务器使用单个线程来处理命令请求,所以如果由服务器直接调用aof_rewrite函数,那么在重写AOF文件期间,服务器将无法处理客户端发来的命令请求。

所以,Redis决定将AOF重写程序放到子进程里执行,这样做可以同时达到两个目的:

  • 子进程AOF重写期间,服务器进程可以继续处理命令请求
  • 子进程带有服务器进程的数据副本,使用子进程而不是线程,避免在使用锁的情况下,保证数据的安全性

不过,使用子进程也有一个问题需要解决,因为子进程在进行AOF重写期间,服务器进程还需要继续处理命令请求,而新的命令可能会对现有的数据库状态进行修改,从而使得服务器当前的数据库状态和重写后的AOF文件所保存的数据库状态不一致。

为了解决这种数据不一致问题,Redis服务器设置了一个AOF重写缓冲区,这个缓冲区在服务器创建子进程之后开始使用,当Redis服务器执行完一个写命令之后,它会同时将这个写命令发送给AOF缓冲区和AOF重写缓冲区

这也就是说,在子进程执行AOF重写期间,服务器进程需要执行以下三个工作:

(1) 执行客户端发来的命令

(2) 将执行后的写命令追加到AOF缓冲区

(3) 将执行后的写命令追加到AOF重写缓冲区。

image.png

这样操作可以保证:

  • AOF缓冲区的内容会定期被写入和同步到AOF文件,对现有AOF文件的处理工作会照常进行
  • 从创建子进程开始,服务器执行的所有写命令都会被记录到AOF重写缓冲区里

当子进程完成AOF重写工作后,会向父进程发送一个信号,父进程在接收该信号后,会调用一个信号处理函数,并执行以下工作:

  • AOF重写缓冲区中的所有内容写入到AOF文件,这时新AOF文件所保存的数据库状态将和服务器当前数据库状态一致
  • 对新的AOF文件进行改名,原子地覆盖现有的AOF文件,完成新旧两个AOF文件的替换

AOF文件后台重写过程

image.png

这也是 BGREWRITEAOF命令的实现原理。

RDB与AOF对比

RDB优势:

  • RDB文件紧凑,全量备份,适合用于进行备份和灾难恢复

  • 生成RDB文件时,Redis会fork()一个子进程处理所有保存工作,主进程不需要进行任何磁盘IO操作

  • RDB在恢复大数据集时比AOF恢复速度快 RDB劣势:

  • 当快照持久化时,会开启子进程专门负责快照持久化,子进程会拥有父进程的内存数据,父进程修改内存子进程不会反应过来,所以在快照持久化期间修改的数据不会被保存,可能丢失数据。

AOF优势:

  • 可以保护数据不丢失,一般AOF会每隔1s,通过一个后台线程执行一次 fsync 操作,最多丢失1s的数据
  • AOF文件没有任何磁盘寻址的开销,写入性能非常高,文件不容易破损
  • AOF文件即使过大时,出现后台重写操作,也不会影响客户端读写
  • AOF文件的命令通过非常可读的方式进行记录,非常适合做灾难性的误删除的紧急恢复

AOF劣势:

  • 对于同一份数据来说,AOF文件通常比RDB数据快照文件更大
  • AOF开启后,支持的写QPS会比RDB支持的写QPS低,因为 AOF 一般会配置成每秒 fsync 一次

小结

命令RDBAOF
启动效率数据集大时效率高数据集小时效率高
文件体积
恢复速度
数据安全性丢数据根据策略决定
轻重

对于两种持久化方式各有利弊,通常都是二者结合使用实现持久化。