Redis持久化

Redis是内存数据库,数据都是存储在内存中,为了避免进程退出导致数据的永久丢失,需要定期将Redis中的数据以某种形式(数据或命令)从内存保存到硬盘,当下次Redis重启时,利用持久化文件实现数据恢复。

Redis持久化分为RDB持久化和AOF持久化:前者将当前数据保存到硬盘,后者则是将每次执行的写命令保存到硬盘(类似于MySQL的binlog)。

RDB

RDB持久化是将当前进程中的数据生成快照保存到硬盘,保存的文件后缀是rdb,当Redis重新启动时,可以读取快照文件恢复数据。RDB持久化分为手动触发和自动触发两种方式:

手动触发

手动触发可以使用save命令和bgsave命令,都可以生成rdb文件。它们的区别在于save命令会阻塞Redis服务器进程,直到RDB文件创建完毕为止,在Redis服务器阻塞期间,服务器不能处理任何命令请求;而bgsave命令会创建一个子进程,由子进程来负责创建RDB文件,父进程(即Redis主进程)则继续处理请求,整个过程中只有fork子进程时会阻塞服务器。

自动触发

自动触发最常见的情况是在配置文件中通过save m n,指定当m秒内发生n次变化时,会触发bgsave。例如默认配置文件中有以下三行:

1
2
3
save 900 1
save 300 10
save 60 10000

只要上面三行任意一条满足时,就会执行bgsave。除此之外,在主从复制的场景下,如果从节点执行全量复制操作,则主节点会执行bgsave命令,并将rdb文件发送给从节点。执行shutdown命令时,也会自动执行rdb持久化。

启动时加载

RDB文件的载入工作是在服务器启动时自动执行的,并没有专门的命令。但是由于AOF的优先级更高,因此当AOF开启时,Redis会优先载入AOF文件来恢复数据;只有当AOF关闭时,才会在Redis服务器启动时检测RDB文件,并自动载入。服务器载入RDB文件期间处于阻塞状态,直到载入完成为止。

AOF

RDB持久化是将进程数据写入文件,而AOF持久化则是将Redis执行的每次写命令记录到单独的日志文件中(有点像MySQL的binlog),当Redis重启时再次执行AOF文件中的命令来恢复数据。与RDB相比,AOF的实时性更好,因此已成为主流的持久化方案。

三种策略

为了提高文件写入效率,在现代操作系统中,当用户将数据写入文件时(write命令),操作系统通常会将数据暂存到一个内存缓冲区里,当缓冲区被填满或超过了指定时限,才真正将缓冲区的数据写入到硬盘里。这样的操作虽然提高了效率,但如果系统崩溃,内存缓冲区中的数据将会丢失。因此可以设置同步选项,强制操作系统什么时候将缓冲区中的数据写入到硬盘中(fsync命令),Redis提供了以下三种同步策略:

  • always:每个写命令都同步
  • everysec:每秒同步一次
  • no:让操作系统来决定何时同步

always会严重降低服务器的性能,而no的不可控性太强,因此Redis使用everysec作为默认配置,但在系统崩溃时可能会丢失一秒的数据。

文件重写

随着Redis服务器执行的写命令越来越多,AOF文件也会越来越大,过大的AOF文件不仅会影响服务器的正常运行,也会导致数据恢复需要的时间过长。文件重写是指定期重写AOF文件,减小AOF文件的体积。需要注意的是,AOF重写是把Redis进程内的数据转化为写命令,同步到新的AOF文件,而不会对旧的AOF文件进行任何读取、写入操作。

文件重写主要是针对以下一些语句:

  • 过期的数据(如expire),可以不用再写入文件。
  • 多次INCR命令可以合并为一个SET命令。
  • 无效的命令不再写入文件,比如有些数据被删除了。

手动触发

可以直接调用bgrewriteaof命令重写文件,该命令的执行与bgsave有些类似,都是fork子进程进行具体的工作,且都只有在fork时阻塞。

自动触发

默认配置是当AOF文件大小是上次重写后大小的一倍(auto-aof-rewrite-min-size)且文件大于64M时触发(auto-aof-rewrite-percentage)。

具体流程

  1. 父进程执行fork操作创建子进程,这个过程中父进程是阻塞的。
  2. 子进程创建后,Redis的所有写命令依然写入AOF缓冲区,并根据设置策略同步到硬盘,保证原有AOF机制的正确。
  3. 由于fork操作使用写时复制技术,子进程只能共享fork操作时的内存数据。由于父进程依然在响应命令,因此Redis使用AOF重写缓冲区(图中的aof_rewrite_buf)保存这部分数据,防止新AOF文件生成期间丢失这部分数据。也就是说,bgrewriteaof执行期间,Redis的写命令同时追加到aof_buf和aof_rewirte_buf两个缓冲区。
  4. 子进程根据内存快照,按照命令合并规则写入到新的AOF文件。
  5. 子进程写完新的AOF文件后,向父进程发信号,父进程把AOF重写缓冲区的数据写入到新的AOF文件,这样就保证了新AOF文件所保存的数据库状态和服务器当前状态一致。
  6. 使用新的AOF文件替换老文件,完成AOF重写。

RDB与AOF对比

  • RDB持久化:RDB文件紧凑,体积小,恢复速度比AOF快很多,但数据的实时性较低。
  • AOF持久化:实时性较高,但是文件大,并且恢复速度较慢,且对性能有一定影响。

常见问题

fork阻塞:CPU的阻塞

在Redis中,无论是RDB持久化的bgsave,还是AOF重写的bgrewriteaof,都需要fork出子进程来进行操作,而在操作系统fork的实现中,基本都采用了写时复制技术,即在父/子进程试图修改数据空间之前,父子进程实际上共享数据空间,但是当父/子进程的任何一个试图修改数据空间时,操作系统会为修改的那一部分(内存的一页)制作一个副本。

也就是说,虽然fork时子进程不会复制父进程的数据空间,但是会复制内存页表,如果Redis内存过大,会导致fork操作时复制内存页表耗时过多,而Redis主进程在进行fork时是完全阻塞的,意味着无法响应客户端的请求,造成请求延迟过大。

为了防止该问题的发生,我们需要控制Redis单机内存的大小,并且适当放宽AOF重写的触发条件,尽量在写入较少的时间段完成重写。

AOF追加阻塞:硬盘的阻塞

AOF持久化过程中,通过fsync命令每秒一次将缓冲区的数据写入磁盘中,但在硬盘负载过高时,fsync操作可能会超过1s,当继续向缓冲区内写入数据时,磁盘负载会越来越大,如果此时Redis进程异常退出,丢失的数据也有可能远超1s。

为此,Redis的处理策略是这样的:主线程每次进行AOF会对比距离上次fsync成功的时间,如果距上次不到2s,主线程直接返回;如果超过2s,则主线程阻塞直到fsync同步完成。因此,如果系统硬盘负载过大导致fsync速度太慢,会导致Redis主线程的阻塞。这里还要注意的是,如果使用everysec策略,AOF最多可能丢失2s的数据,而不是1s。

参考资料