源码级别了解 Redis 持久化

文章首发于公众号“蘑菇睡不着”,欢迎来访~

前言

  你们都知道 Redis 是一个内存数据库,数据都存储在内存中,这也是 Redis 很是快的缘由之一。虽然速度提上来了,可是若是数据一直放在内存中,是很是容易丢失的。好比 服务器关闭或宕机了,内存中的数据就木有了。为了解决这一问题,Redis 提供了 持久化 机制。分别是 RDB 以及 AOF 持久化。c++

RDB

什么是 RDB 持久化?

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

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 1
save 300 10
save 60 10000
上边三个配置的含义是:服务器

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

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

struct redisServer{

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

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

struct saveparam {

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

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

struct redisServer {
    // ...
    
    // 修改计数器
    long long dirty;
    
    // 上一次执行保存时间
     time_t lastsave;
     
     // ...
}
  • dirty 计数器记录距离上一次成功执行 SAVE 或 BGSAVE 命令以后,服务器对数据库状态(服务器中的全部数据库)进行了多少次修改(包括写入、删除、更新等操做)。
  • lastsave 属性是一个 UNIX 时间戳,记录了服务器上一次执行 SAVE 或 BGSAVE 命令的时间。

检查条件是否知足触发 RDB

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

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

RDB 文件结构

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

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。

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\n$3\r\nSET\r\n$3\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 选项的值 flushAppendOnlyFile 函数的行为
always 将 aof_buf 缓冲区中的全部内容写入并同步到 AOF 文件
everysec 将 aof_buf 缓冲区中的全部内容写入到 AOF 文件,若是上次同步 AOF 文件的时间距离如今超过一秒钟,那么再次对 AOF 文件进行同步,而且这个同步操做是由一个线程专门负责执行的
no 将 aof_buf 缓冲区中的全部内容写入到 AOF 文件,但并不对 AOF 文件进行同步,什么时候同步由操做系统来决定

若是用户没有主动为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文件所保存的数据库状态就会被完整地还原出来,整个过程 以下图所示。

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重写缓冲区,以下图:

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

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

这样一来能够保证:

  • AOF缓冲区的内容会按期被写入和同步到AOF文件,对现有AOF文件的处理工做会如常 进行。
  • 从建立子进程开始,服务器执行的全部写命令都会被记录到AOF重写缓冲区里面。

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

  1. 将AOF重写缓冲区中的全部内容写入到新AOF文件中,这时新AOF文件所保存的数 据库状态将和服务器当前的数据库状态一致。
  2. 对新的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 全量文件重放,重启效率所以大幅获得提高。

以为文章不错的话,小伙伴们麻烦点个赞、关个注、转个发一下呗~你的支持就是我写文章的动力。

更多精彩的文章请关注公众号“蘑菇睡不着”。

你越主动就会越主动,咱们下期见~

相关文章
相关标签/搜索