Redis持久化机制

为了能够重用Redis数据,或者防止系统故障,我们需要将Redis中的数据写到磁盘空间中,即持久化。
Redis提供了两种不同的持久化方法可以将数据存储在磁盘中,一种叫快照RDB,另一种叫只追加文件AOF

AOF日志

以日志的形式来记录每个写操作,将Redis执行的所以写指令记录下来不记录读的指令因为没意义,只许追加文件但不可改写文件,Redis启动之初会读取该文件重新构建数据,换言之,Redis重启的话根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
image.png
在Redis中AOF默认不开启,需要我们修改redis.conf配置文件中的一下参数:
image.png
AOF日志就是普通文件,可以通过cat命令查看里面的内容。

image.png
注:Redis先执行写命令操作后,才将该命令记录到AOF日志里面的,这么做的好处是
1.避免额外的检查开销(只有当操作命令执行成功才写入到日志,防止检查,因为先写入日志中,数据恢复的时候还要检查命令语法是否正确之类的)
2.不会阻塞当前写操作命令的执行(因为在写操作完成后写入到日志)

AOF也存在风险:
第一个风险:执行写操作和记录日志是两个过程,那当Redis在还没来及将命令写入到硬盘时,服务器宕机了,这个数据久会有丢失的风险
第二个风险:由于写操作成功命令成功后才记录到AOF日志,所以不会当前的写操作命令。但是会给下一次命令带来阻塞。

image.png

如果在将日志内容写入到硬盘时,服务器的硬盘I/O的压力太大了,就会导致硬盘的速度很慢,进而阻塞住了,也就会导致后续的命令无法执行。

三种写回策略

image.png

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

深入到源码可以知道,这三种策略只是在控制fsync()函数调用的时间
当应用程序向文件写入数据时,内核通常先将数据复制到内核缓冲区中,然后排入队列,然后再由内核绝对何时写入硬盘。
image.png

  • Always策略就是每次写入AOF文件数据后,就执行fsync()函数
  • EverySec策略就会创建一个异步任务来执行fsync()函数
  • No用不执行fsync()函数

    AOF重写机制

    AOF毕竟是一个文件,随着执行的写操作命令越来越多时,文件大小会越来越大,如果当AOF日志文件过大就会带来性能问题,比如重启的时候,需要读取AOF文件的内容以恢复数据,如果文件过大,整个恢复的过程就很慢。
    所以为了AOF文件越来越大,提供了AOF重写机制,当文件大小达到某个阈值的时候,Redis就会启动AOF重写机制,来压缩AOF文件。
    AOF重写机制时在重写的时,读取当前数据库中的所有键值对,然后将每个键值对用一条命令记录到新的AOF文件,等到全部记录完后,将新的AOF文件替换现有的AOF文件
    比如执行了两次set name 123 ,set name 456,就会把最新的记录记录到AOF文件中,之前的操作就没必要记录了。
    重写的时候需要先写入到新的AOF文件,不直接复用现有的AOF文件,因为如果AOF重写失败造成AOF文件被污染。

    AOF后台重写

    写入AOF日志的操作虽然是在主线程中进行的,但是它写入的内容不多,所以一般不影响命令的操作。
    但是在触发重写AOF重写时,这时是需要读取所有缓存的见键值对数据的,并为每一个键值对生成一条命令。然后将其写入新的AOF文件中,重写完后,就把现在的AOF文件替换掉,这个过程很耗时,所以重写的操作不能放在主进程中
    所以AOF重写的操作是由后台子进程bgwriteof来完成的,这样做的好处有两个

  • 子进程在进行AOF重写期间,主进程可以继续处理命令请求,从而避免阻塞主进程

  • 子进程带有主进程的数据副本,这里使用子进程而不是线程,因为如果是使用线程,多线程之间会共享内存,那么在修改内存数据的时候,需要加锁来保证数据的安全,从而降低性能,而使用子进程创建子进程时,父子之间共享的是内存,不管这个共享的内存数据是只读的方式,而父子的任意一方修改了共享内存,都会发生【写时复制】,于是父子进程就有了独立的数据副本,就不用加锁来保证数据安全了。

子进程是怎么拥有主进程的一样的副本数据的呢?
主进程在通过fork系统调用生成bgrewriteaof子进程时,操作系统会把主进程的页表,复制一份给子进程,这个页表记录的虚拟地址和物理地址映射关系,而不会复制物理内存,也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个。
image.png

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

不过,当父进程或者子进程在向这个内存发起写操作时,CPU 就会触发缺页中断,这个缺页中断是由于违反权限导致的,然后操作系统会在「缺页异常处理函数」里进行物理内存的复制,并重新设置其内存映射关系,将父子进程的内存读写权限设置为可读写,最后才会对内存进行写操作,这个过程被称为「写时复制(Copy On Write)」。
image.png
写时复制顾名思义,在发生写操作的时候,操作系统才会去复制物理内存,这样是为了防止 fork 创建子进程时,由于物理内存数据的复制时间过长而导致父进程长时间阻塞的问题。
所以,有两个阶段会导致阻塞父进程:
1.创建子进程的途中,由于要复制父进程的页表等数据结构,阻塞的时间跟页表的大小有关,页表越大,阻塞的时间也越长;
2.创建完子进程后,如果子进程或者父进程修改了共享数据,就会发生写时复制,这期间会拷贝物理内存,如果内存越大,自然阻塞的时间也越长;

触发重写机制后,主进程就会创建重写 AOF 的子进程,此时父子进程共享物理内存,重写子进程只会对这个内存进行只读,重写 AOF 子进程会读取数据库里的所有数据,并逐一把内存数据的键值对转换成一条命令,再将命令记录到重写日志(新的 AOF 文件)。
但是子进程重写过程中,主进程依然可以正常处理命令
如果此时主进程修改了已经存在 key-value,就会发生写时复制,注意这里只会复制主进程修改的物理内存数据,没修改物理内存还是与子进程共享的。

所以如果这个阶段修改的是一个 bigkey,也就是数据量比较大的 key-value 的时候,这时复制的物理内存数据的过程就会比较耗时,有阻塞主进程的风险。

还有个问题,重写 AOF 日志过程中,如果主进程修改了已经存在 key-value,此时这个 key-value 数据在子进程的内存数据就跟主进程的内存数据不一致了,这时要怎么办呢?

为了解决这种数据不一致问题,Redis 设置了一个 AOF 重写缓冲区,这个缓冲区在创建 bgrewriteaof 子进程之后开始使用。

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

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

执行客户端发来的命令;
将执行后的写命令追加到 「AOF 缓冲区」;
将执行后的写命令追加到 「AOF 重写缓冲区」;
当子进程完成 AOF 重写工作(扫描数据库中所有数据,逐一把内存数据的键值对转换成一条命令,再将命令记录到重写日志)后,会向主进程发送一条信号,信号是进程间通讯的一种方式,且是异步的。

主进程收到该信号后,会调用一个信号处理函数,该函数主要做以下工作:

将 AOF 重写缓冲区中的所有内容追加到新的 AOF 的文件中,使得新旧两个 AOF 文件所保存的数据库状态一致;
新的 AOF 的文件进行改名,覆盖现有的 AOF 文件。
信号函数执行完后,主进程就可以继续像往常一样处理命令了。

在整个 AOF 后台重写过程中,除了发生写时复制会对主进程造成阻塞,还有信号处理函数执行时也会对主进程造成阻塞,在其它时候。AOF后台重写都不会阻塞主进程。

RDB

快照怎么用?

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

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

Redis 的快照是全量快照,也就是说每次执行快照,都是把内存中的「所有数据」都记录到磁盘中。
所以可以认为,执行快照是一个比较重的操作,如果频率太频繁,可能会对 Redis 性能产生影响。如果频率太低,服务器故障时,丢失的数据会更多。
通常可能设置至少 5 分钟才保存一次快照,这时如果 Redis 出现宕机等情况,则意味着最多可能丢失 5 分钟数据。
这就是 RDB 快照的缺点,在服务器发生故障时,丢失的数据会比 AOF 持久化的方式更多,因为 RDB 快照是全量快照的方式,因此执行的频率不能太频繁,否则会影响 Redis 性能,而 AOF 日志可以以秒级的方式记录操作命令,所以丢失的数据就相对更少。

执行快照时,数据能被修改么?

可以,写时复制技术,执行bgsave命令的时候,会通过fork()创建子进程,此时子进程和父进程是共享同一片内存数据的,因为创建子进程的时候,会复制进程的页表,但是页表指向的物理内存还是一个。
image.png
只有在发生修改内存数据的情况时,物理内存才会被复制一份。
这样的目的是为了减少创建子进程时的性能损耗,从而加快创建子进程的速度,毕竟创建子进程的过程中,是会阻塞主线程的。所以,创建 bgsave 子进程后,由于共享父进程的所有内存数据,于是就可以直接读取主线程里的内存数据,并将数据写入到 RDB 文件。
但是,如果主线程要修改共享数据里的某一块数据(比如键值对 A)时,就会发生写时复制,于是这块数据的物理内存就会被复制一份(键值对 A’),然后主线程在这个数据副本(键值对 A’)进行修改操作。与此同时,bgsave 子进程可以继续把原来的数据(键值对 A)写入到 RDB 文件
就是这样,Redis 使用 bgsave 对当前内存中的所有数据做快照,这个操作是由 bgsave 子进程在后台完成的,执行时不会阻塞主线程,这就使得主线程同时可以修改数据。
细心的同学,肯定发现了,bgsave 快照过程中,如果主线程修改了共享数据,发生了写时复制后,RDB 快照保存的是原本的内存数据,而主线程刚修改的数据,是被办法在这一时间写入 RDB 文件的,只能交由下一次的 bgsave 快照。

ROB和AOF组合

尽管 RDB 比 AOF 的数据恢复速度快,但是快照的频率不好把握:

  • 如果频率太低,两次快照间一旦服务器发生宕机,就可能会比较多的数据丢失;
  • 如果频率太高,频繁写入磁盘和创建子进程会带来额外的性能开销。

    那有没有什么方法不仅有 RDB 恢复速度快的优点和,又有 AOF 丢失数据少的优点呢?
    当然有,那就是将 RDB 和 AOF 合体使用,这个方法是在 Redis 4.0 提出的,该方法叫混合使用 AOF 日志和内存快照,也叫混合持久化。
    如果想要开启混合持久化功能,可以在 Redis 配置文件将下面这个配置项设置成 yes:

    1. aof-use-rdb-preamble yes

    也就是说,使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据
    image.png
    这样的好处在于,重启 Redis 加载数据的时候,由于前半部分是 RDB 内容,这样加载的时候速度会很快
    加载完 RDB 的内容后,才会加载后半部分的 AOF 内容,这里的内容是 Redis 后台子进程重写 AOF 期间,主线程处理的操作命令,可以使得数据更少的丢失