redis持久化机制

orbisz2025/8/11后端学习Redis

AOF日志

AOF持久化:Redis每执行一条写操作命令,就把该命令以追加的方式写入一个文件里。注意只会记录写操作命令,不会记录读命令。 AOF默认不开启,可以修改redis.conf中的参数:

appendonly      yes //表示是否开启持久化
appendfilename  "appendonly.aof" //AOF持久化文件的名称

可以通过cat命令查看aof文件的内容。

Redis是先执行写操作命令后,才将该命令记录到AOF日志里的

  • 避免额外的检查开销:如果先将写操作命令记录到AOF日志里,再执行该命令的话,如果当前的命令语法有问题,那么如果不进行命令语法检查,该错误的命令记录到AOF日志里后,Redis在使用日志恢复数据时,就可能会出错。
  • 不会阻塞当前写操作命令的执行:因为当写操作命令执行成功后,才会将命令记录到 AOF 日志。

注意,AOF持久化功能也是有风险的

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

因为将命令写入到日志的这个操作也是在主进程完成的,所以执行命令和写日志是同步的。如果在将日志内容写入到硬盘时,服务器的硬盘的 I/O 压力太大,就会导致写硬盘的速度很慢,进而阻塞住了,也就会导致后续的命令无法执行。

Redis写入AOF日志得过程

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

三种写回策略

redis提供了三种写回磁盘的策略,在 redis.conf 配置文件中的 appendfsync 配置

写回策略写回时机优点缺点
Always同步写回可靠性高、最大程度保证数不丢失每个写命令都要写回硬盘,性能开销大
Everysec每秒写回性能适中宕机时会丢失1秒内的数据
No由操作系统控制写回性能好宕机时丢失的数据可能会很多

了解源码后可以发现,其实这三种策略知识在控制 fsync() 函数调用的时机。

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

AOF重写机制

AOF 日志是一个文件,随着写操作命令越多,文件大小就会越大,也就会带来性能问题,Redis 恢复过程就会很慢。 AOF 重写就通过压缩 AOF 文件来避免文件过大的问题。

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

重写机制的妙处在于,尽管某个键值对被多条写命令反复修改,最终也只需要根据这个「键值对」当前的最新状态,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令,这样就减少了 AOF 文件中的命令数量。最后在重写工作完成后,将新的 AOF 文件覆盖现有的 AOF 文件。

重写 AOF 的时候,之所以不直接复用现有的 AOF 文件,而是先写到新的 AOF 文件再覆盖过去。是担心 AOF 重写过程失败的话,会对现有的 AOF 文件造成污染。

后台重写

Redis的重写过程比较耗时,是由后台子进程bgrewriteaof来完成

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

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

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

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

当父进程或者子进程在向物理内存发起写操作时,CPU 就会触发缺页中断,这个缺页中断是由于违反权限导致的,然后操作系统会在「缺页异常处理函数」里进行物理内存的复制,并重新设置其内存映射关系,将父子进程的内存读写权限设置为可读写,最后才会对内存进行写操作, 这个过程被称为「写时复制(Copy On Write)」。 写时复制顾名思义,在发生写操作的时候,操作系统才会去复制物理内存,这样是为了防止 fork 创建子进程时,由于物理内存数据的复制时间过长而导致父进程长时间阻塞的问题。

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

创建子进程的途中,操作系统复制父进程页表的时候,父进程是阻塞的。 创建完子进程后,如果子进程或者父进程修改了共享数据,就会发生写时复制,这期间会拷贝物理内存,如果内存越大,自然阻塞的时间也越长;

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

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

AOF缓冲区

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

Redis 设置了一个 AOF 重写缓冲区,这个缓冲区在创建 bgrewriteaof 子进程之后开始使用。 即在重写 AOF 期间,当 Redis 执行完一个写命令之后,它会同时将这个写命令写入到 「AOF 缓冲区」和 「AOF 重写缓冲区」。

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

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

当子进程完成 AOF 重写工作后,会向主进程发送一条信号。主进程收到该信号后,会调用一个信号处理函数,该函数主要做以下工作:

  • 将 AOF 重写缓冲区中的所有内容追加到新的 AOF 的文件中,使得新旧两个 AOF 文件所保存的数据库状态一致;
  • 新的 AOF 的文件进行改名,覆盖现有的 AOF 文件。

信号函数执行完后,主进程就可以继续像往常一样处理命令了。 在整个 AOF 后台重写过程中,除了发生写时复制会对主进程造成阻塞,还有信号处理函数执行时也会对主进程造成阻塞,在其他时候,AOF 后台重写都不会阻塞主进程。

RDB快照

Redis 可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本,记录的实际数据。提供了两个命令生成 RDB 文件:

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

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

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

执行 bgsave 过程中,Redis 依然可以继续处理操作命令的,也就是数据是能被修改的。关键就在于写时复制技术.

执行 bgsave 命令的时候,会通过 fork() 创建子进程,此时子进程和父进程是共享同一片内存数据的,因为创建子进程的时候,会复制父进程的页表,但是页表指向的物理内存还是一个。只有在发生修改内存数据的情况时,物理内存才会被复制一份。

这样可以减少创建子进程时的性能损耗,从而加快创建子进程的速度。

创建 bgsave 子进程后,由于共享父进程的所有内存数据,于是就可以直接读取主线程里的内存数据,并将数据写入到 RDB 文件。 当主线程对这些共享的内存数据也都是只读操作,那么,主线程和 bgsave 子进程相互不影响。

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

就是这样,Redis 使用 bgsave 对当前内存中的所有数据做快照,这个操作是由 bgsave 子进程在后台完成的,执行时不会阻塞主线程,这就使得主线程同时可以修改数据。

快照过程中,如果主线程修改了共享数据,那么主线程的数据和RDB文件中的数据是不同步的

写时复制极端情况:

在 Redis 执行 RDB 持久化期间,刚 fork 时,主进程和子进程共享同一物理内存,但是途中主进程处理了写操作,修改了共享内存,于是当前被修改的数据的物理内存就会被复制一份。 那么极端情况下,如果所有的共享内存都被修改,则此时的内存占用是原先的 2 倍。

混合持久化机制

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

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

Redis 4.0 提出了混合使用 AOF 日志和内存快照,也叫混合持久化。通过配置文件中的配置项:aof-use-rdb-preamble yes.

混合持久化工作在AOF日志重写过程。

当开启了混合持久化时,在 AOF 重写日志时,fork 出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件, 然后主线程处理的操作命令会被记录在重写缓冲区里,重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件, 写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。

也就是说,使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据。

这样的好处在于,重启 Redis 加载数据的时候,由于前半部分是 RDB 内容,这样加载的时候速度会很快。

加载完 RDB 的内容后,才会加载后半部分的 AOF 内容,这里的内容是 Redis 后台子进程重写 AOF 期间,主线程处理的操作命令,可以使得数据更少的丢失。