源码级别理解 Redis 持久化

前言

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

RDB

什么是 RDB 持久化?

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

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 选项设置的保存条件:

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 源码解析如下:

程序会遍历并检查 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文件并还原数据库状态的详细步骤如下:

创建一个不带网络连接的伪客户端(fake client):因为Redis的命令只能在客户端上 下文中执行,而载入AOF文件时所使用的命令直接来源于AOF文件而不是网络连接,所以服 务器使用了一个没有网络连接的伪客户端来执行AOF文件保存的写命令,伪客户端执行命令 的效果和带网络连接的客户端执行命令的效果完全一样。

从AOF文件中分析并读取出一条写命令。

使用伪客户端执行被读出的写命令。

一直执行步骤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重写期间,服务器进程需要执行以下三个工作:

执行客户端发来的命令。

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

将执行后的写命令追加到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 全量文件重放,重启效率因此大幅得到提升。

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

最后小编在学习过程中整理了一些学习资料,可以分享给做java的工程师朋友们,相互交流学习,需要的可以加入我的学习交流群323432957即可免费获取Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容