Redis 持久化

Redis 持久化简介

持久化就是把内存的数据写到磁盘中,防止服务器宕机导致内存数据丢失。

Redis 支持两种方式的持久化,一种是RDB的方式,一种是AOF的方式。

RDB 持久化

RDB 就是 Redis DataBase 的缩写,中文名为快照 / 内存快照,RDB持久化是把当前进程数据生成快照保存到磁盘上的过程,由于是某一时刻的快照,那么快照的值要早于或等于内存的值。

触发方式

触发RDB持久化的方式有两种,分别是手动触发和自动触发

手动触发

手动触发分别对应 save 和 bgsave 命令。

  • save命令:阻塞当前 Redis 服务器,直到 RDB 过程完成为止,对于内存比较大的实例会造成长时间的阻塞,线上环境不建议使用。
  • bgsave命令:Redis 进程执行 fork 操作创建子进程,RDB 持久化过程由 子进程 负责,完成后自动结束。阻塞只发生在 fork 阶段,一般时间很短。

bgsave流程图如下所示:

Redis 持久化

执行过程:

  • 执行bgsave命令
  • Redis 父进程判断当前是否存在正在执行的子进程,如果存在,bgsave命令直接返回。
  • 父进程执行fork操作创建子进程fork 操作过程中父进程会阻塞
  • 父进程 fork 完成后,父进程继续接收并处理客户端的请求,而子进程开始将内存中的数据写进硬盘的临时数据
  • 当子进程写完所有数据后会用该临时文件替换旧的RDB文件

自动触发

在以下四种情况时会自动触发。

  • redis.conf 中配置 save m n ,即在 m 秒内有 n 次修改时,自动触发 bgsave 生成 rdb 文件。
  • 主从复制时, 从节点 要从 主节点 进行 全量复制 时,也会触发bgsave操作,生成当时的快照发送到从节点。
  • 执行 debug reload 命令重新加载 redis 时如果没有开启 aof 持久化,那么也会触发 bgsave
  • 默认情况下执行 shutdown 命令时,如果没有开启 aof 持久化,那么也会触发 bgsave 操作。

深入理解

  • 由于生产环境中我们为Redis开辟的内存区域都比较大(例如6GB),那么将内存中的数据同步到硬盘的过程可能就会持续比较长的时间,而实际情况是这段时间Redis服务一般都会收到数据写操作的请求。那么如何保证数据一致性呢?

RDB 中的核心思路是 Copy-on-Write (写时复制),来保证在进行快照操作的这段时间,需要压缩写入磁盘上的数据在内存中不会发生变化。在正常的快照操作中,一方面 Redis 主进程 会 fork 一个新的快照进程专门来做这个事情,这样保证了 Redis 服务 不会停止对客户端包括写请求在内的任何响应。另一方面,在 fork 过程中发生的数据变化会以副本的方式存放在另一个新的内存区域,待快照操作结束后才会同步到原来的内存区域。

举个例子:如果主线程对这些数据也都是读操作(例如图中的键值对A),那么,主线程 和 bgsave 子进程相互不影响。但是,如果主线程要修改一块数据(例如图中的键值对C),那么,这块数据就会被复制一份,生成该数据的副本。然后,bgsave 子进程会把这个副本数据写入到 rdb 文件,而在这个过程中,主线程仍然可以直接修改原来的数据。

Redis 持久化

  • 在进行快照操作的这段时间,如果发生服务崩溃怎么办?

在没有将数据全部写入到磁盘前,这次快照操作都不算成功。如果出现了服务崩溃的情况,将以上一次完整的 RDB 快照文件 作为回复内存数据的参考。也就是说,在快照操作过程中不能影响上一次的备份数据。Redis服务会在磁盘上创建一个临时文件进行数据操作,待操作成功后才会用这个临时文件替换掉上一次的备份。

  • 可以每秒做一次快照吗?

显然是不可以的,虽然 bgsave 执行时不会阻塞主线程,但是,如果频繁的执行全量快照,也会带来两方面的开销:

  • 频繁的将全量数据写入磁盘,会给磁盘带来很大的压力,多个快照竞争有限的磁盘带宽,前一个快照还没有做完,后一个又开始做了,容易造成恶性循环。
  • bgsave 子进程需要通过 fork 操作从主线程创建出来。虽然,子进程在创建后不会再阻塞主线程,但是,fork 这个创建进程的操作本身 会阻塞主线程,而且主线程的内存越大,阻塞时间越长。如果频繁的 fork 出 bgsave 子进程,这就会频繁的阻塞主线程了。

RDB优缺点

  • 优点:

    • Redis 加载 RDB 文件恢复数据要远远快于 AOF 方式。
    • RDB 文件是某个时间节点的快照,默认使用LZF算法进行压缩,压缩后的文件体积远远小于内存大小,适用于备份、全量复制等场景。
  • 缺点:

    • RDB 方式的实时性不够,无法做到秒级持久化
    • 每次调用 bgsave 都需要 fork子进程,fork子进程 属于重量级操作,频繁执行成本较高
    • RDB 文件是 二进制 的,没有可读性,AOF 文件在了解其结构的情况下可以手动修改或者补全。
    • 版本兼容RDB文件问题,Redis 版本升级过程中有多个格式的 RDB 版本,存在老版本 Redis 无法兼容 新版 RDB格式的问题

AOF 持久化

Redis 是“写后”日志,Redis 先执行命令,把数据写入内存,然后才记录日志。日志里记录的是Redis收到的每一条命令,这些命令是以文本形式保存。

PS:大多数的数据库采用的是写前日志(WAL),例如MySQL,通过写前日志和两阶段提交,实现数据和逻辑的一致性。

而 AOF 日志采用写后日志,即先写内存,后写日志

为什么采用写后日志?

Redis 要求高性能,采用写日志的有两方面好处

  • 避免额外的检查开销:Redis 在向AOF里面记录日志的时候,并不会先对这些命令进行语法检查。所以,如果先记日志再执行命令的话,日志中就有可能记录了错误的命令,Redis在使用日志恢复数据时,就可能会出错。
  • 不会阻塞当前的写操作。

这种方式存在一定风险:

  • 如果命令执行完成,写日志之前宕机了,会丢失数据。
  • 主线程写磁盘压力大,导致写盘慢,阻塞后续操作。

AOF的实现

AOF日志记录 Redis 的每个写指令,步骤分为:命令追加(append)、文件写入(write)和 文件同步(sync)。

  • 命令追加:当 AOF 持久化功能打开时,服务器在执行完一个 写命令 之后,会以协议格式将被执行的写命令追加到服务器的 aof_buf 缓冲区。
  • 文件写入和同步:关于何时将 aof_buf 缓冲区的内容写入 AOF 文件中,Redis提供了三种写回策略:

Redis 持久化

Always同步写回:每个写命令执行完,立马同步地将日志写回磁盘;

Everysec每秒写回:每个写命令执行完,只是先把日志写到 AOF文件 的内容缓冲区,每隔 1s 把缓冲区的内容写入磁盘;

No操作系统控制的写回:每个写命令执行完,只是先把日志写到 AOF 文件的内容缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。

  • 三种写回策略的优缺点

上面的三种策略体现了一个重要原则:trade-off,取舍,指在性能和可靠性保证之间做取舍。

关于 AOF 的同步策略是涉及到操作系统的 write 函数 和 fsync 函数的,在《Redis设计与实现》是这样说明的。

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

这样的操作虽然提高了效率,但也为数据写入带来了安全问题:如果计算机停机,内存缓冲区中的数据会丢失。为此,系统提供了fsync、fdatasync同步函数,可以强制操作系统立刻将缓冲区中的数据写入到硬盘中,从而确保写入数据的安全性。

深入理解AOF重写

Redis 通过创建一个新的 AOF 文件来替换现有的 AOF ,新旧两个 AOF 文件保存的数据相同,但新 AOF 文件中没有冗余命令。

Redis 持久化

  • AOF 重写会阻塞吗?

AOF重写过程是由后台进程bgrewriteaof来完成的。主线程 fork 出后台的 bgrewriteaof 子进程,fork 会把主线程的内存拷贝一份给bgrewriteaof 子进程,这里面就包含了数据库的最新数据。然后,bgrewriteaof 子进程就可以在不影响主线程的情况下,逐一把拷贝的数据写成操作,记入重写日志。

所以 AOF 在重写时,在 fork 进程时是会阻塞主线程的。

  • AOF 日志何时会重写?

有两个配置项控制AOF重写的触发:

  1. auto-aof-rewrite-min-size:表示运行AOF重写时 文件的最小大小,默认为64MB。
  2. auto-aof-rewrite-percentage:这个值的计算方式是,当前 aof 文件大小和上一次重写后 aof 文件大小的差值,再除以上一次重写后 aof 文件大小。也就是当前 aof文件 比上一次重写后 aof文件 的增量大小,和上一次重写后 aof文件 大小的比值。
  • 重写日志时,有新数据写入怎么处理?

重写过程总结为:“一个拷贝,两处日志”。在 fork 出子进程时的拷贝,以及在重写时,如果有新数据写入,主线程就会将命令记录到两个 aof 日志内存缓冲区中。如果 AOF 写回策略配置的是 always ,则直接将命令写回旧的日志文件,并且保存一份命令至 AOF 重写缓冲区,这些操作对新的日志文件是不存在影响的。(旧的日志文件:主线程使用的日志文件,新的日志文件: bgrewriteaof 进程使用的日志文件)。

而在 bgrewriteaof 子进程完成对日志文件的重写操作后,会提示主线程已经完成重写操作,主线程会将 AOF 重写缓冲区中的命令追加到新的日志文件后面。这时候在高并发的情况下,AOF 重写缓冲区积累可能会很大,这样就会造成阻塞,Redis后来通过 Linux 管道技术让 aof 重写期间就能同时进行回放,这样 aof 重写结束后只需回放少量剩余数据即可。

最后通过修改文件名的方式,保证文件切换的原子性。

在 AOF 重写日志期间发生宕机的话,因为日志文件还没切换,所以恢复数据时,用的还是旧的日志文件。

从持久化中恢复数据

Redis 持久化

流程如下:

  • redis 重启时判断是否开启 aof ,如果开启 aof ,那么就优先加载 aof 文件;
  • 如果 aof 存在,那么就去加载 aof 文件,加载成功的话 redis 重启成功,如果 aof 文件加载失败,那么就会打印日志表示启动失败,此时可以去修复 aof 文件后重新启动;
  • 若 aof 文件不存在,那么 redis 就会转去加载 rdb 文件,如果 rdb 文件不存在,redis 直接启动成功;
  • 如果 rdb 文件存在就会去加载 rdb 文件恢复数据,若加载失败则打印日志提示启动失败,若加载成功,那么 redis 重启成功,且使用 rdb 文件恢复数据;

那么为什么会优先加载 aof 文件呢?

因为 aof 保存的数据更完整,因为 aof 基本上最多损失 1s 的数据。

上一篇:redis


下一篇:Redis 基础知识介绍