【Redis】八股总结四:持久化RDB、AOF

107 阅读14分钟

【Redis】五、Redis持久化、RDB和AOF-CSDN博客

小林coding,Redis持久化

问题

aof日志的实现

Redis在执行完一条写操作命令后,就会把该命令以追加的方式写入到一个文件里(aof都是主线程执行

然后Redis重启时,会读取该文件记录的命令,然后逐一执行命令的方式来进行数据恢复

AOF写其实不一定是直接写到磁盘

  1. Redis 执行完写操作命令后,会将命令追加到 server.aof_buf 用户缓冲区;
  2. 然后通过 write() 系统调用,将 aof_buf 缓冲区的数据写入到 AOF 文件,此时数据并没有写入到硬盘,而是拷贝到了内核缓冲区 page cache等待内核将数据写入硬盘
  3. 具体内核缓冲区的数据什么时候写入到硬盘( 执行fsync()系统调用 ),由内核决定。

通过appendfsync 配置项,有三种策略,always,no和everysecond

RDB快照的实现

RDB 快照就是记录某一个瞬间的redis内存数据,记录的是实际数据,而 AOF 文件记录的是命令操作的日志,而不是实际的数据。

因此在 Redis 恢复数据时, RDB 恢复数据的效率会比 AOF 高些,因为直接将 RDB 文件读入内存就可以,不需要像 AOF 那样还需要额外执行操作命令的步骤才能恢复数据。

实现:

Redis 提供了两个命令来生成 RDB 文件,分别是 save 和 bgsave,他们的区别就在于是否在「主线程」里执行

  • 执行了 save 命令,就会在主线程生成 RDB 文件,由于和执行操作命令在同一个线程,所以如果写入 RDB 文件的时间太长,会阻塞主线程
  • 执行了 bgsave 命令,会创建一个子进程来生成 RDB 文件,这样可以避免主线程的阻塞

AOF的重写

  1. 后台进程读取数据库中所有键值对,对每一个键值对用一条命令记录它的写入。
  2. 日志文件变小,具有“多变一”的功能,旧日志文件中的多条命令,重写后在新日志中变成了一条命令。

RDB触发时机

  1. 配置文件中save规则满足,60s 内修改了 5次key(别看选项名叫 save,实际上执行的是 bgsave 命令)
  2. 执行flushall命令
  3. redis关闭时

Redis宕机数据会丢失吗?

讲aof和rdb,最后讲4.0之后的混合模式

RDB是主进程执行吗,AOF是主进程执行吗,AOF是主进程执行吗?

RDB快照:save命令是主进程进行快照,bgsave是子进程进行快照,一般用bgsave子进程

AOF日志aof是主进程,aof主进程默认写到pageCache,如果通过参数设置每次都写到磁盘(写回策略),那么可能会阻塞主进程影响后续执行的修改命令( 影响性能,处理并发能力降低

AOF重写:aof重写不是主进程,是后台进程。

RDB和AOF的比较

可以先看rdb和aof,再看这个。

RDB 比 AOF 优秀的地方

  • RDB比AOF文件更小,适合做数据的备份,灾难恢复。当 AOF 变得太大时,Redis 能够在后台自动重写 AOF。新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样,但体积更小。
  • 与 AOF 相比,恢复大数据集的时候,RDB 速度更快。因为rdb是快照,而aof记录的命令需要一条一条执行。

AOF 比 RDB 优秀的地方:

  • RDB 的数据安全性不如 AOF,没有办法实时或者秒级持久化数据。AOF 支持秒级数据丢失(取决 fsync 策略, 如果是 everysec,最多丢失 1 秒的数据)
  • AOF 以一种易于理解和解析的格式包含所有操作的日志。你可以轻松地导出 AOF 文件进行分析,你也可以直接操作 AOF 文件来解决一些问题。

总结

  1. 如果丢失一点数据 也没关系的话 可以用RDB
  2. 如果对数据安全性较高,那就rdb和aof都开启。
  3. 不建议只开启AOF,因为RDB快照可以数据备份、重启更快、还用于主从复制

RDB

RDB 快照就是记录某一个瞬间的内存数据,记录的是实际数据,而 AOF 文件记录的是命令操作的日志,而不是实际的数据。

因此在 Redis 恢复数据时, RDB 恢复数据的效率会比 AOF 高些,因为直接将 RDB 文件读入内存就可以,不需要像 AOF 那样还需要额外执行操作命令的步骤才能恢复数据。

快照

Redis 提供了两个命令来生成 RDB 文件,分别是 save 和 bgsave,他们的区别就在于是否在「主线程」里执行

  • 执行了 save 命令,就会在主线程生成 RDB 文件,由于和执行操作命令在同一个线程,所以如果写入 RDB 文件的时间太长,会阻塞主线程
  • 执行了 bgsave 命令,会fork一个子进程来生成 RDB 文件,这样可以避免主线程的阻塞

RDB 文件的加载工作是在服务器启动时自动执行的,Redis 并没有提供专门用于加载 RDB 文件的命令。

Redis 的快照是全量快照,也就是说每次执行快照,都是把内存中的「所有数据」都记录到磁盘中。

所以可以认为,执行快照是一个比较重的操作,如果频率太频繁,可能会对 Redis 性能产生影响。如果频率太低,服务器故障时,丢失的数据会更多。


执行快照时 数据能被修改吗?(先了解)

直接说结论吧,执行 bgsave 过程中,Redis 依然可以继续处理操作命令的,也就是数据是能被修改的。

那具体如何做到到呢?关键的技术就在于写时复制技术(Copy-On-Write, COW)。

通过 fork() 创建子进程,此时子进程和父进程是共享同一片内存数据的,因为创建子进程的时候,会复制父进程的页表,但是页表指向的物理内存还是一个。

创建 bgsave 子进程后,由于共享父进程的所有内存数据,于是就可以直接读取主线程(父进程)里的内存数据,并将数据写入到 RDB 文件。

但是,如果主线程(父进程)要修改共享数据里的某一块数据(比如键值对 A)时,就会发生写时复制,于是这块数据的物理内存就会被复制一份(键值对 A' ,然后主线程在这个数据副本(键值对 A' )进行修改操作。与此同时,bgsave 子进程可以继续把原来的数据(键值对 A )写入到 RDB 文件

Redis 在使用 bgsave 快照过程中,如果主线程修改了内存数据,不管是否是共享的内存数据,RDB 快照都无法写入主线程刚修改的数据,因为此时主线程(父进程)的内存数据和子进程的内存数据已经分离了,子进程写入到 RDB 文件的内存数据只能是原本的内存数据。

如果系统恰好在 RDB 快照文件创建完毕后崩溃了,那么 Redis 将会丢失主线程在快照期间修改的数据

AOF

AOF 持久化是怎么实现的?

Redis 每执行一条写操作命令,就把该命令以追加的方式写入到一个文件里,然后重启 Redis 的时候,先去读取这个文件里的命令,并且执行它。

这种保存写操作命令到日志的持久化方式,就是 Redis 里的 AOF( Append Only File ) 持久化功能


Redis 是先执行写操作命令后,才将该命令记录到 AOF 日志里的。这么做其实有两个好处。

第一个好处,避免额外的检查开销。 只有在该命令执行成功后,才将命令记录到 AOF 日志里,这样就不用额外的检查开销,保证记录在 AOF 日志里的命令都是可执行并且正确的。

第二个好处,不会阻塞当前写操作命令的执行,因为当写操作命令执行成功后,才会将命令记录到 AOF 日志。

但是可能会给「下一个」命令带来阻塞风险。因为AOF 指向命令和记录日志是同步的,是主线程执行的日志,如果写磁盘特别慢会阻塞下一个命令。

那么怎么办?其实就是AOF日志写回硬盘时机的问题,有一下三种策略

三种写回策略

先说写回步骤

  1. Redis 执行完写操作命令后,会将命令追加到 server.aof_buf 缓冲区;
  2. 然后通过 write() 系统调用,将 aof_buf 缓冲区的数据写入到 AOF 文件,此时数据并没有写入到硬盘,而是拷贝到了内核缓冲区 page cache等待内核将数据写入硬盘
  3. 具体内核缓冲区的数据什么时候写入到硬盘( 执行fsync()系统调用 ),由内核决定。

Redis 提供了 3 种写回硬盘的策略,控制的就是上面说的第三步的过程

在 redis.conf 配置文件中的 appendfsync 配置项可以有以下 3 种参数可填:

  • Always,这个单词的意思是**「总是」**,所以它的意思是每次写操作命令执行完后,同步将 AOF 日志数据写回硬盘;
  • Everysec,这个单词的意思是**「每秒」,所以它的意思是每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,然后每隔一秒将缓冲区里的内容写回到硬盘**;
  • No,意味着不由 Redis 控制写回硬盘的时机,转交给操作系统控制写回的时机,也就是每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区再由操作系统决定何时将缓冲区内容写回硬盘。

这 3 种写回策略都无法能完美解决「主进程阻塞」和「减少数据丢失」的问题,因为两个问题是对立的,偏向于一边的话,就会要牺牲另外一边

深入到源码后,你就会发现这三种策略只是在控制 fsync() 函数的调用时机。

aof重写机制

  1. 后台进程读取数据库中所有键值对,对每一个键值对用一条命令记录它的写入
  2. 将日志文件变小,具有“多变一”的功能,旧日志文件中的多条命令,重写后在新日志中变成了一条命令。

AOF 重写机制是在重写时,读取当前数据库中的所有键值对 ,然后将每一个键值对用一条命令记录到新的 AOF 文件」,等到全部记录完后,就将新的 AOF 文件替换掉现有的 AOF 文件。

因为之前的aof是记录的修改命令,因为多个修改命令对同一个数据操作之后只有一个结果,所以对某个 数据重写后的命令只有一条 set key value。相当于把多个命令减少记录成一个命令。

重写机制的妙处在于,尽管某个键值对被多条写命令反复修改,最终也只需要根据这个「键值对」当前的最新状态,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令

后台进程重写

注意是后台进程重写,不是主进程。

Redis 的重写 AOF 过程是由后台子进程 bgrewriteaof 来完成的,这么做可以达到两个好处

  • 子进程进行 AOF 重写期间,主进程可以继续处理命令请求,从而避免阻塞主进程
  • 子进程带有主进程的数据副本(数据副本怎么产生的后面会说),这里使用子进程而不是线程,因为如果是使用线程,多线程之间会共享内存,那么在修改共享内存数据的时候,需要通过加锁来保证数据的安全,而这样就会降低性能。而使用子进程,创建子进程时,父子进程是共享内存数据的,不过这个共享的内存只能以只读的方式,而当父子进程任意一方修改了该共享内存,就会发生「写时复制 ,于是父子进程就有了独立的数据副本,就不用加锁来保证数据安全。

子进程的数据副本 和 写时复制 (先了解即可)

子进程是怎么拥有主进程一样的数据副本的呢?

主进程在通过 fork 系统调用生成 bgrewriteaof 子进程时,操作系统会把主进程的「页表」复制一份给子进程,这个页表记录着虚拟地址和物理地址映射关系,而不会复制物理内存,也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个。

这样一来,子进程就共享了父进程的物理内存数据了,这样能够节约物理内存资源,页表对应的页表项的属性会标记该物理内存的权限为只读

不过,当父进程或者子进程在向这个内存发起写操作时,CPU 就会触发写保护中断,这个写保护中断是由于违反权限导致的,然后操作系统会在**「写保护中断处理函数」里进行物理内存的复制**,并重新设置其内存映射关系,将父子进程的内存读写权限设置为可读写,最后才会对内存进行写操作,这个过程被称为「写时复制( Copy On Write ) 」。

写时复制顾名思义,在发生写操作的时候,操作系统才会去复制物理内存,这样是为了防止 fork 创建子进程时,由于物理内存数据的复制时间过长而导致父进程长时间阻塞的问题。

不过,如果父进程的内存数据非常大,那自然页表也会很大,这时父进程在通过 fork 创建子进程的时候,阻塞的时间也越久。

所以,有两个阶段会导致阻塞父进程:

  • fork创建子进程的途中,由于要复制父进程的页表等数据结构,阻塞的时间跟页表的大小有关,页表越大,阻塞的时间也越长;
  • 创建完子进程后,如果子进程或者父进程修改了共享数据,就会发生写时复制,这期间会拷贝物理内存,如果内存越大,自然阻塞的时间也越长;

子进程重写过程中,主进程依然可以正常处理命令。

如果此时主进程修改了已经存在 key-value,就会发生写时复制,注意这里只会复制主进程修改的物理内存数据,没修改物理内存还是与子进程共享的

还有个问题,重写 AOF 日志过程中,如果主进程修改了已经存在 key-value,此时这个 key-value 数据在子进程的内存数据就跟主进程的内存数据不一致了,为了解决这种数据不一致问题,Redis 设置了一个 AOF 重写缓冲区

在重写 AOF 期间,当 Redis 执行完一个写命令之后,它会同时将这个写命令写入到 「AOF 缓冲区」和 「AOF 重写缓冲区」

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

  • 执行客户端发来的命令;
  • 将执行后的写命令追加到 「AOF 缓冲区」;
  • 将执行后的写命令追加到 「AOF 重写缓冲区」;