7
头图

一、Redis 持久化简介

Redis 的持久化功能是区别于 Memcached 显著特性,数据持久化可以保证系统在发生宕机和重启后数据不会丢失,对于 redis 这种存储在内存中的数据库显得尤为重要。 在 Redis 4.0 以前数据持久化的方式主要有两种

  • RDB(Redis DataBase)快照方式: 它是将某一时刻的内存数据以二进制的方式写入磁盘,默认保存文件为 dump.rdb
  • AOF(Append Only File)文件追加方式:它是指将所有的操作命令,以文本的形式追加到文件中。默认保存文件是 appendonly.aof

二、RDB 持久化机制

RDB 是将内存中的数据在某一时刻的状态记录下来以二进制的方式存储到磁盘中,通过生成一个经过压缩的二进制文件来实现数据库状态的复原。默认是 dump.rdb 文件,它的优点是以二进制存储,占用空间更小,数据存储更紧凑。

因为 RDB 文件是保存在磁盘中,哪怕 Redis 服务器宕机,Redis 服务器就可以通过该文件来还原数据库状态。

2.1 RDB 触发方式

触发RDB持久化既可以通过手动执行,也可以根据服务器配置选项定期执行。主要分为手动触发和自动触发两种方式。

2.1.1 手动触发

手动触发主要是通过save 和 bgsave 两个命令来生成RDB 文件

  1. save 命令
  • 会阻塞当前 Redis 服务器,然后直到 RDB 文件创建完毕为止 。对于内存较大的实例会造成长时间阻塞。
  1. bgsave 命令
  • 该命令会派生fork出一个子进程,由子进程负责创建RDB文件,服务器(父线程)继续处理命令请求。RDB 持久化过程由子进程负责,完成后自动结束,阻塞阶段只发生在 fork 阶段,阻塞时间较短。所以大多数情况使用bgsave 命令生成RDB文件

2.1.2 自动触发

自动触发主要是通过 redis.conf 配置文件来完成:

# save <seconds> <changes> 周期性执行RDB持久化条件的设置格式
# 默认配置
save 900 1
save 300 10
save 60 10000

# 文件名称
dbfilename dump.rdb

# 文件保存路径
dir /home/work/app/redis/data/

# 如果持久化出错,主进程是否停止写入
stop-writes-on-bgsave-error yes

# 是否压缩
rdbcompression yes

# 导入时是否检查,这里是使用CRC64算法来进行数据校验,会增加性能消耗
rdbchecksum yes

还有其他情况也可能会导致自动触发:

  • 主从复制时,从节点要从主节点进行全量复制时也会触发 bgsave 操作,生成当时的快照 RDB 文件发送到从节点
  • 执行debug reload 命令重新加载 redis 时也会触发 bgsave 操作,生成 RDB 文件
  • 默认情况下执行 shutdown 命令时,若没有开启 AOF 持久化,也会触发 bgsave 操作,生成 RDB 文件

2.2 RDB 实战问题

1.在某一时刻给数据量较大内存进行快照,此时服务器也会收到数据写请求,如何保证数据一致性?

数据量较大时进行快照,用时相对会比较长。如果服务器在这个期间收到写请求,那么就不能保证快照的完整性。那么Redis 是如何做的?

Redis 使用的是操作系统提供的写时复制技术(Copy-On-Write, COW),在执行快照的同时,正常处理写操作。

如下图,针对 bgsave 命令触发 RDB 机制来说,如果主线程要修改一块数据(图中的键值对 C),这块数据就会被复制一份,生成该数据的副本(键值对C')。此时中线程在该副本上进行修改,bgsave 子线程继续把原来的数据(键值对 C)写入 RDB 文件中。

这样既保证了RDB 文件的完整性,也允许主线程同时对数据进行修改,避免了对正常业务的影响。

2.在进行 RDB 快照的过程中,发生服务崩溃了怎么办?

数据没有全部写入磁盘前,该次快照操作都是失败的。将以这次快照操作前的完整 RDB 快照文件作为恢复内存的数据。也就是此次快照操作过程不能影响上一次的备份数据。实际上在操作时 Redis 服务会在磁盘上创建一个临时文件来进行数据操作,待操作成功后才会用这个临时文件替换上一次的备份数据。

3.可以每秒做一次快照吗?

频繁的执行全量快照,能够带来更快的恢复速度。但同时也会带来大量的开销,给磁盘带来压力,多个快照竞争有限的磁盘带宽,容易造成恶性循环。此外, bgsave 子进程需要通过 fork 操作从主线程创建出来。如果频繁的fork 出 bgsave 子进程就会阻塞主线程。因此 Redis 在有一个 bgsave 运行时,就不会再启动第二个 bgsave 子进程。

这里就可以采用增量快照的方法,就是在全量快照后,后续的快照只对修改后的数据进行快照,避免对数据重复的那部分进行快照带来的消耗,这样更加高效。这里就需要对修改的文件数据进行记录,如下图所示:

但是,我们在增量快照时,记录所修改的数据信息也是一部分的开销,在大量数据修改时的记录数据,其内存开销也不少。所以对于做快照的频率,不能太高也不能太低。那么有什么解决方法既能够让 RDB 快速恢复,又能以较小的开销实现少丢数据呢?

Redis 4.0 提出了 混合使用 AOF 日志和内存快照的方法可以很好的解决上面的场景。

三、AOF 持久化机制

AOF (Append Only File)日志和大多数的数据库写入日志的方式不同,采用的是写后日志。(比如 MySQL 是通过写前日志(Write Ahead Log, WAL)和两阶段提交来实现数据和逻辑的一致性)

写后日志的意思是 Redis 先将数据写入内存,然后再记录日志,如下图:

采用后写日志有这样的好处:

  • 避免额外的检查开销:比如在我们输入命令时可能会出错,如果先记录日志再执行命令,日志中可能记录错误的命令,在利用日志恢复数据时可能会出错。那先命令,后记录日志就可以当命令执行成功再记录,避免出现记录错误命令的情况
  • 不会阻塞当前写操作:因为它是在命令执行后才记录日志,不会执行当前的写操作

但是在写回磁盘时也会产生风险:

  • 在命令执行和写日志中间出现宕机,执行命令数据会发生丢失
  • 虽然避免当前命令的阻塞,但是可能会给下一个操作带来阻塞风险。磁盘写压力大,会导致写盘很慢,进而导致后续操作无法执行。

这两个风险是 AOF 写回磁盘时机不当造成的,对于这两个风险,AOF 有三种写回策略可以避免。也就是在文件写入同步时发生。

3.1 AOF 持久化的实现

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

3.1.1 命令追加

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

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

以Redis 服务端收到 set testkey testvalue 命令后的记录为例,查看 AOF 日志的运行原理。

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

*3\r\n$3\r\nSET\r\n$7\r\ntestkey\r\n$9\r\ntestvalue\r\n

  • *3 :表示当前命令有三个部分,每个部分都是由$+数字开头,后面紧跟具体的命令、键或值
  • \r\n:是换行符,具体如上图所示
  • $+数字:数字表示这部分的命令、键或值一共有多少个字节,比如 $3 set 表示这部分有三个字节的命令,也就是 set 命令

3.1.2 文件写入和同步

Redis 的服务器进程就是一个事件循环(loop),在处理文件事件时可能会执行写命令,所以在服务器每次结束一个事件循环之前,它会调用 flushAppendOnlyFile 函数,考虑是否需要将 aof_buf 缓冲区中的内容写入到AOF 文件中去。

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

flushAppendOnlyFile函数的行为有服务器配置的 appendfsync 选项的值来决定。其值主要有三种:

3.1.3 AOF 文件的载入与数据还原

在保存了 AOF 文件后,服务器只需要重新执行 AOF 文件中的写命令后,就可以还原服务器关闭之前的数据库状态,其具体步骤如下:

  1. 创建一个不带网络连接的伪客户端(fake client)
  2. 从 AOF 文件中分析并读取一条写命令
  3. 使用 fake client 执行被读出的 写命令
  4. 重复执行步骤2和3,直到 AOF 命令中所有写命令都被处理完毕

3.2 AOF 重写机制

因为 AOF 是通过保存被执行的写命令来完成 Redis 持久化的,所以随着 Redis 的运行时间越长,AOF 文件中存储的日志内容会越来越多。会给 Redis 服务器,带来很大的影响。因此 Redis 提供了 AOF 文件重写功能解决这一痛点。

3.2.1 重写机制是如何减少 AOF 文件大小的?

重写机制具有“多变一”的功能,也就是对旧日志文件中的多条命令,在重写后的新日志变成了一条命令。如下图所示:

3.2.2 重写会阻塞吗?

AOF 重写过程是由后台子进程 bgrewriteaof 来完成,重写过程是一个拷贝,两处日志:

  • 一个拷贝:是指每次执行重写时,主线程 fork 出后台的 bgrewriteaof 子进程。此时,fork 会把主线程的内存拷贝一份给 bgrewriteaof。所以这个子线程就可以在不影响主线程的情况下,逐一把拷贝的数据写成操作,记入重写日志。
  • 两处日志:

    • 第一处日志指的是正在使用的 AOF 日志,Redis 会把这个操作写到它的缓冲区。即使宕机了,这个 AOF 日志的操作仍然是齐全的可以用于恢复。
    • 第二处日志指的是新 AOF 重写日志,这个操作也会被写到重写日志的缓冲区。这样,重写日志也不会丢失最新的操作

所以说在每次 AOF 重写时,Redis 会先执行一份内存拷贝,用于重写;然后使用两个日志保证在重写过程中新写入的数据不会丢失。同时因为 bgrewriteaof 子进程的出现,这个过程并不会阻塞主线程。

3.2.3 AOF 日志何时会进行重写?

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

  • auto-aof-rewrite-min-size: 表示运行 AOF 重写时文件的最小大小,默认是 64MB
  • auto-aof-rewrite-percentage: 这个值是当前 aof 文件和上一次重写后 aof 文件差值,再除以上一次重写 aof 文件大小。

四、RDB 和 AOF 区别与联系

4.1 RDB 和 AOF 混合机制

在介绍 RDB 机制时,我们了解到是否有一种方法既能利用 RDB 的快速恢复,又能以较小的开销做到尽量少丢数据?也就是混合使用 RDB 和 AOF 两种机制的方法:就是让 RDB 快照以一定频率执行,在两次快照之间使用 AOF 日志记录这期间的所有命令操作。

如上图所示,第一次全量快照后,在T1 和 T2 时刻修改用 AOF 日志记录,等到第二次全量快照时,可以清空 AOF 日志。因为此时的修改都已经记录到快照中,恢复时也就不再使用日志了。

4.2 RDB 和 AOF 优缺点

RDB

  • 默认文件是 dump.rdb。具备更快速的数据重启恢复能力,以二进制存储占用更小的磁盘空间,但是有数据丢失的风险

AOF

  • 默认保存文件是 appendonly.aof。存储频率更高,存储信息更容易懂,缺点是占用空间大,重启之后的数据恢复速度较慢。

所以 RDB 和 AOF 混合机制综合了上述两种机制的优缺点。

4.3 RDB 和 AOF 文件的载入和还原

AOF 的载入和还原过程在 AOF 小节中有提到。现在谈谈同时存在两者的情况,服务器如何用哪个文件来还原数据库状态:

  • 如果服务器开启了 AOF 持久化功能,优先使用 AOF 文件,因为AOF 更新频率通常要比 RDB 文件要高
  • 只有当 AOF 持久化功能关闭时,服务器才会使用 RDB 文件来还原数据状态

参考资料:

《redis 设计与实现》

https://baijiahao.baidu.com/s?id=1654694618189745916&wfr=spid...

https://time.geekbang.org/column/article/271839


归思君
1.2k 声望209 粉丝

阿里云社区专家博主,华为云社区云享专家,一个会点前端的java工程师。