阿里架构师纯手写最新Redis持久化实战手册,看完让你精通Redis

230 阅读18分钟

Hello,今天给各位童鞋们分享的是Redis持久化,赶紧拿出小本子记下来吧

image.png

前言

  大家都知道 Redis 是一个内存数据库,数据都存储在内存中,这也是 Redis 非常快的原因之一。虽然速度提上来了,但是如果数据一直放在内存中,是非常容易丢失的。比如 服务器关闭或宕机了,内存中的数据就木有了。为了解决这一问题,Redis 提供了 持久化 机制。分别是 「RDB」 以及 「AOF」 持久化。

RDB

什么是 RDB 持久化?

RDB 持久化可以在指定的时间间隔内生成数据集的时间点快照(point-in-time snapshot)。

image.png

RDB 的优点?

  • RDB 是一种表示某个即时点的 Redis 数据的紧凑文件。RDB 文件适用于备份。例如,你可能想要每小时归档最近 24 小时的 RDB 文件,每天保存近 30 天的 RDB 快照。这允许你很容易的恢复不同版本的数据集以容灾。
  • RDB 非常适合于灾难恢复,作为一个紧凑的单一文件,可以被传输到远程的数据中心。
  • RDB 最大化了 Redis 的性能。因为 Redis 父进程持久化时唯一需要做的是启动(fork)一个子进程,由子进程完成所有剩余的工作。父进程实例不需要执行像磁盘 IO 这样的操作。
  • RDB 在重启保存了大数据集的实例比 AOF 快。

RDB 的缺点?

  • 当你需要在 Redis 停止工作(例如停电)时最小化数据丢失,RDB 可能不太好。你可以配置不同的保存点(save point)来保存 RDB 文件(例如,至少 5 分钟和对数据集 100 次写之后,但是你可以有多个保存点)。然而,你通常每隔 5 分钟或更久创建一个 RDB 快照,所以一旦 Redis 因为任何原因没有正确关闭而停止工作,你就得做好最近几分钟数据丢失的准备了。
  • RDB 需要经常调用 fork()子进程来持久化到磁盘。如果数据集很大的话,fork()比较耗时,结果就是,当数据集非常大并且 CPU 性能不够强大的话,Redis 会停止服务客户端几毫秒甚至一秒。AOF 也需要 fork(),但是你可以调整多久频率重写日志而不会有损(trade-off)持久性(durability)。

RDB 文件的创建与载入

  有个两个 Redis 命令可以用于生成 RDB 文件,一个是 「SAVE」,另一个是 「BGSAVE」。  「SAVE」 命令会阻塞 Redis 服务器进程,直到 RDB 文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令请求。

 SAVE     // 一直等到 RDB 文件创建完毕 OK

和 SAVE 命令直接阻塞服务器进程不同的是,BGSAVE 命令会派生出一个子进程,然后由子进程负责创建 RDB 文件,服务器进程(父进程)继续处理命令进程。「执行 fork 的时候操作系统(类 Unix 操作系统)会使用写时复制(copy-on-write)策略,即 fork 函数发生的一刻父子进程共享同一内存数据,当父进程要更改其中某片数据时(如执行一个写命令 ),操作系统会将该片数据复制一份以保证子进程的数据不受影响,所以新的 RDB 文件存储的是执行 fork 一刻的内存数据。」

 BGSAVE  // 派生子进程,并由子进程创建 RDB 文件 Background saving started

生成 RDB 文件由两种方式:「一种是手动,就是上边介绍的用命令的方式;另一种是自动的方式。」接下来详细介绍一下自动生成 RDB 文件的流程。Redis 允许用户通过设置服务器配置的 「save」 选项,让服务器每隔一段时间自动执行一次 BGSAVE 命令。用户可以通过在 redis.conf 配置文件中的 SNAPSHOTTING 下 save 选项设置多个保存条件,但只要其中任意一个条件被满足,服务器就会执行 BGSAEVE 命令。如,以下配置:save 900 1save 300 10save 60 10000 上边三个配置的含义是:

  • 服务器在 900 秒内,对数据库进行了至少 1 次修改。
  • 服务器在 300 秒内,对数据库进行了至少 10 次修改。
  • 服务器在 60 秒内,对数据库进行了至少 10000 次修改。

如果没有手动去配置 save 选项,那么服务器会为 save 选项配置默认参数:save 900 1save 300 10save 60 10000 接着,服务器就会根据 save 选项的配置,去设置服务器状态 redisServer 结构的 saveparams 属性:

struct redisServer{

  // ...      // 记录了保存条件的数组   struct saveparams *saveparams;      // ... };

saveparams 属性是一个数组,数组中的每一个元素都是一个 saveparam 结构,每个 saveparam 结构都保存了一个 save 选项设置的保存条件:

struct saveparam {

  // 秒数   time_t seconds;      // 修改数   int changes; };

除了 saveparams 数组之外,服务器状态还维持着一个 dirty 计数器,以及一个 lastsave 属性;

struct redisServer {     // ...          // 修改计数器     long long dirty;          // 上一次执行保存时间      time_t lastsave;            // ... }

dirty 计数器记录距离上一次成功执行 SAVE 或 BGSAVE 命令之后,服务器对数据库状态(服务器中的所有数据库)进行了多少次修改(包括写入、删除、更新等操作)。

lastsave 属性是一个 UNIX 时间戳,记录了服务器上一次执行 SAVE 或 BGSAVE 命令的时间。

检查条件是否满足触发 RDB

Redis 的服务器周期性操作函数 serverCron 默认每隔 100 毫秒执行一次,该函数用于对正在运行的服务器进行维护,它的其中一项工作就是检查 save 选项所设置的保存条件是否已经满足,如果满足的话就执行 BGSAVE 命令。Redis serverCron 源码解析如下:

image.png

程序会遍历并检查 saveparams 数组中的所有保存条件,只要有任意一个条件被满足,服务器就会执行 BGSAVE 命令。下面是 rdbSaveBackground 的源码流程:

image.png

RDB 文件结构

下图展示了一个完整 RDB 文件所包含的各个部分。

image.png redis 文件的最开头是 「REDIS」 部分,这个部分的长度是 5 字节,保存着 “REDIS” 五个字符。通过这五个字符,程序可以在载入文件时,快速检查所载入的文件是否时 RDB 文件。

「db_version」 长度为 4 字节,他的值时一个字符串表示的整数,这个整数记录了 RDB 文件的版本号,比如 “0006” 就代表 RDB 文件的版本为第六版。

「database」 部分包含着零个或任意多个数据库,以及各个数据库中的键值对数据:

如果服务器的数据库状态为空(所有数据库都是空的),那么这个部分也为空,长度为 0 字节。

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

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

「check_sum」 是一个 8 字节长的无符号整数,保存着一个校验和,这个校验和时程序通过对 REDIS、db_version、database、EOF 四个部分的内容进行计算得出的。服务器在载入 RDB 文件时,会将载入数据所计算出的校验和与 check_sum 所记录的校验和进行对比,以此来检查 RDB 是否有出错或者损坏的情况。举个例子:下图是一个 0 号数据库和 3 号数据库的 RDB 文件。第一个就是 “REDIS” 表示是一个 RDB 文件,之后的 “0006” 表示这是第六版的 REDIS 文件,然后是两个数据库,之后就是 EOF 结束标识符,最后就是 check_sum。

image.png

AOF 持久化

什么是 AOF 持久化

AOF 持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF 命令以 redis 协议追加保存每次写的操作到文件末尾.Redis 还能对 AOF 文件进行后台重写,使得 AOF 文件的体积不至于过大.

AOF 的优点?

  • 使用 AOF 会让你的 Redis 更加耐久: 你可以使用不同的 fsync 策略:无 fsync,每秒 fsync,每次写的时候 fsync.使用默认的每秒 fsync 策略,Redis 的性能依然很好(fsync 是由后台线程进行处理的,主线程会尽力处理客户端请求),一旦出现故障,你最多丢失 1 秒的数据.
  • AOF 文件是一个只进行追加的日志文件,所以不需要写入 seek,即使由于某些原因(磁盘空间已满,写的过程中宕机等等)未执行完整的写入命令,你也也可使用 redis-check-aof 工具修复这些问题.
  • Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写: 重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。 整个重写操作是绝对安全的,因为 Redis 在创建新 AOF 文件的过程中,会继续将命令追加到现有的 AOF 文件里面,即使重写过程中发生停机,现有的 AOF 文件也不会丢失。 而一旦新 AOF 文件创建完毕,Redis 就会从旧 AOF 文件切换到新 AOF 文件,并开始对新 AOF 文件进行追加操作。
  • AOF 文件有序地保存了对数据库执行的所有写入操作, 这些写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂, 对文件进行分析(parse)也很轻松。 导出(export) AOF 文件也非常简单: 举个例子, 如果你不小心执行了 FLUSHALL 命令, 但只要 AOF 文件未被重写, 那么只要停止服务器, 移除 AOF 文件末尾的 FLUSHALL 命令, 并重启 Redis , 就可以将数据集恢复到 FLUSHALL 执行之前的状态。

AOF 的缺点?

  • 对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积。
  • 根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB 。 在一般情况下, 每秒 fsync 的性能依然非常高, 而关闭 fsync 可以让 AOF 的速度和 RDB 一样快, 即使在高负荷之下也是如此。 不过在处理巨大的写入载入时,RDB 可以提供更有保证的最大延迟时间(latency)。

AOF 持久化的实现

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

命令追加

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

struct redisServer {   // ...   // AOF 缓冲区     sds aof_buf;      // .. };

如果客户端向服务器发送以下命令:

 set KEY VALUE OK

那么服务器在执行这个 set 命令之后,会将以下协议内容追加到 aof_buf 缓冲区的末尾;

*3\r\n3\nSET˚\n˚3\r\nSET\r\n3\r\nKEY\r\n$5\r\nVALUE\r\n

AOF 文件的写入与同步

  Redis 的服务器进程就是一个事件循环(loop),这个循环中的文件事件负责接收客户端的命令请求,以及向客户端发送命令回复,而时间事件则负责执行像 serverCron 函数这样需要定时运行的函数。  因为服务器在处理文件事件时可能会执行写命令,使得一些内容被追加到 aof_buf 缓冲区里面,所以在服务器每次结束一个事件循环之前,它都会调用 「flushAppendOnlyFile」 函数,考虑是否需要将 aof_buf 缓冲区中的内容写入和保存到 AOF 文件里面,这个过程可以用以下伪代码表示:

def eventLoop():   while True:      #处理文件事件,接收命令请求以及发送命令回复   #处理命令请求时可能会有新内容被追加到 aof_buf缓冲区中   processFileEvents()      #处理时间事件   processTimeEvents()      #考虑是否要将 aof_buf中的内容写入和保存到 AOF文件里面   flushAppendOnlyFile()

flushAppendOnlyFile 函数的行为由服务器配置的 「appendfsync」 选项的值来决定,各个不同值产生的行为如下表所示。

如果用户没有主动为 appendfsync 选项设置值,那么 appendfsync 选项的默认值为 everysec。写到这里有的小伙伴可能会对上面说的写入和同步含义弄混,这里说一下:「写入:将 aof_buf 中的数据写入到 AOF 文件中。」「同步:调用 fsync 以及 fdatasync 函数,将 AOF 文件中的数据保存到磁盘中。」通俗地讲就是,你要往一个文件写东西,写的过程就是写入,而同步则是将文件保存,数据落到磁盘上。大家之前看文章的时候是不是大多都说 AOF 最多丢失一秒钟的数据,那是因为 redis AOF 默认是 everysec 策略,这个策略每秒执行一次,所以 AOF 持久化最多丢失一秒钟的数据。

AOF 文件的载入与数据还原

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

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

image.png

AOF 重写

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

 rpush list "A" "B" OK  rpush list "C" OK  rpush list "D" OK  rpush list "E" "F" OK

  那么光是为了记录这个 list 键的状态,AOF 文件就需要保存四条命令。  对于实际的应用程度来说,写命令执行的次数和频率会比上面的简单示例要高得多,所 以造成的问题也会严重得多。 为了解决 AOF 文件体积膨胀的问题,Redis 提供了 AOF 文件重写(rewrite)功能。通过该 功能,Redis 服务器可以创建一个新的 AOF 文件来替代现有的 AOF 文件,新旧两个 AOF 文件所 保存的数据库状态相同,但新 AOF 文件不会包含任何浪费空间的冗余命令,所以新 AOF 文件 的体积通常会比旧 AOF 文件的体积要小得多。 在接下来的内容中,我们将介绍 AOF 文件重写的实现原理,以及 BGREWEITEAOF 命令 的实现原理。  虽然 Redis 将生成新 AOF 文件替换旧 AOF 文件的功能命名为“AOF 文件重写”,但实际上, AOF 文件重写并不需要对现有的 AOF 文件进行任何读取、分析或者写入操作,这个功能是通 过读取服务器当前的数据库状态来实现的。  就像上面的情况,服务器完全可以将这六条命令合并成一条。

 rpush list "A" "B" "C" "D" "E" "F"

除了上面列举的列表键之外,其他所有类型的键都可以用同样的方法去减少 AOF 文件中的命令数量。首先从数据库中读取键现在的值,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令,这就是 AOF 重写功能的实现原理。  在实际中,为了避免在执行命令时造成客户端输入缓冲区溢出,重写程序在处理列表、 哈希表、集合、有序集合这四种可能会带有多个元素的键时,会先检查键所包含的元素数 量,如果元素的数量超过了 redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD 常量的值,那 么重写程序将使用多条命令来记录键的值,而不单单使用一条命令。 在目前版本中,REDIS_AOF_REWRITE_ITEMS_PER_CMD 常量的值为 64,这也就是 说,如果一个集合键包含了超过 64 个元素,那么重写程序会用多条 SADD 命令来记录这个集 合,并且每条命令设置的元素数量也为 64 个。

AOF 后台重写

  AOF 重写会执行大量的写操作,这样会影响主线程,所以 redis AOF 重写放到了子进程去执行。这样可以达到两个目的:

子进程进行 AOF 重写期间,服务器进程(父进程)可以继续处理命令请求。

子进程带有服务器进程的数据副本,使用子进程而不是线程,可以在避免使用锁的情况 下,保证数据的安全性。

但是有一个问题,当子进程重写数据时,主进程依然在处理新的数据,这也就会造成数据不一致情况。「为了解决这种数据不一致问题,Redis 服务器设置了一个 AOF 重写缓冲区,这个缓冲区在 服务器创建子进程之后开始使用,当 Redis 服务器执行完一个写命令之后,它会同时将这个写 命令发送给 AOF 缓冲区和 AOF 重写缓冲区」,如下图:

image.png

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

  1. 执行客户端发来的命令。
  2. 将执行后的写命令追加到 AOF 缓冲区。
  3. 将执行后的写命令追加到 AOF 重写缓冲区。

这样一来可以保证:

AOF 缓冲区的内容会定期被写入和同步到 AOF 文件,对现有 AOF 文件的处理工作会如常 进行。

从创建子进程开始,服务器执行的所有写命令都会被记录到 AOF 重写缓冲区里面。

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

将 AOF 重写缓冲区中的所有内容写入到新 AOF 文件中,这时新 AOF 文件所保存的数 据库状态将和服务器当前的数据库状态一致。

对新的 AOF 文件进行改名,原子地(atomic)覆盖现有的 AOF 文件,完成新旧两个 AOF 文件的替换。

这个信号处理函数执行完毕之后,父进程就可以继续像往常一样接受命令请求了。在整个 AOF 后台重写过程中,只有信号处理函数执行时会对服务器进程(父进程)造成 阻塞,在其他时候,AOF 后台重写都不会阻塞父进程,这将 AOF 重写对服务器性能造成的影 响降到了最低。

Redis 混合持久化

Redis 还可以同时使用 AOF 持久化和 RDB 持久化。 在这种情况下, 当 Redis 重启时, 它会优先使用 AOF 文件来还原数据集, 因为 AOF 文件保存的数据集通常比 RDB 文件所保存的数据集更完整。但是 AOF 恢复比较慢,Redis 4.0 推出了「混合持久化」。

混合持久化: 将 rdb 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是 自持久化开始到持久化结束 的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小。

于是在 Redis 重启的时候,可以先加载 「RDB」 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。

好啦,今天的文章就到这里了,希望能够帮助到屏幕前迷茫的你们