目录
RDB(Redis Database)
RDB是Redis的一种数据持久化到硬盘的策略,是一种以内存快照形式保存Redis数据的方式。所谓快照,就是把某一时刻的状态以文件的形式进行全量备份到一盘,这个快照文件就称为RDB文件。
Q:在进行数据备份的时候会不会阻塞主线程影响其他请求?
A:Redis提供了两个命令save和bgsave来生成RDB文件。save命令会阻塞主线程直到RDB文件创建完成为止。在此期间,Redis不能处理客户端的任何请求。
bgsave命令会创建一个子进程,然后由子进程负责专门写入RDB,主进程继续处理命令请求,不会被阻塞(在fork过程中主进程是会阻塞的,但是通常情况下该指令执行的速度比较快,对性能影响不大)。
Q:在执行bgsave过程中数据还能被修改吗?
A:执行bgsave过程中,Redis 依然可以继续处理操作命令的,也就是数据是能被修改的。它用的是COW(copy-on-write)技术,具体会在下文AOF详解。
AOF(Append Only File)
AOF日志的生成过程
从我们发送写指令开始到指令保存在AOF文件中,需要经历4步,分别为命令传播、命令追加、文件写入和文件同步。
命令传播
Redis 将执行完的命令、命令的参数、命令的参数个数等信息发送到 AOF程序中。
Q:AOF日志写入是在Redis成功执行命令之后才进行的,为什么要在执行之后而不是之前?
A:
- 如果不小心输错了Redis指令并且直接将该指令保存到了AOF文件中,等到Redis进行数据恢复的时候就可能导致错误,因此这种先执行后写日志的形式可以避免对指令进行语法检查,避免出现记录错误指令的情况。
- 先执行命令后保存日志不会阻塞当前的写操作。
但是先执行指令再写日志也并非是完美的:
- 假设说Redis在执行完指令后突然宕机,此时AOF日志还未来得及写入,则该条指令和相关参数就有丢失的风险。
- 如果硬盘的I/O花费时间过长必然会阻塞后续的操作。
因此Redis引入了缓冲区的概念,缓冲区对应了文件的写入方式(不求一步到位,允许循序渐进地写入),而何时将缓冲区的内容彻底同步到文件就涉及到了AOF的同步策略(写回磁盘的时机)。
命令追加
在AOF开启的情况下,Redis会将成功执行的写指令append到aof_buf缓冲区。
文件写入
当用户调用write()将数据写入到文件时,操作系统内核会将数据首先保存在Page Cache中,等到缓冲区的空间被填满或者到达一定的时机之后,内核会将数据同步到磁盘。这种同步过于依赖于操作系统内核,时机无法掌控。为此,操作系统提供了fsync和fdatasync两个同步函数,可以强制内核立即将缓冲区内的数据同步到磁盘。
具体的流程如下:
文件同步
redis.conf配置文件中appendfsync的选项有三个值可选,对应三种AOF回写策略,分别是No、Everysec、Always。三种策略无非就是针对性能和丢失率的一个取舍。
No:同步时机由内核决定
由操作系统内核决定同步时机,每个写命令执行完,只是先把日志写入
AOF
文件的内核缓冲区,不立即进行同步。在这种模式下, 同步只会在以下任意一种情况下被执行:
- Redis被关闭
- AOF功能被关闭
- 系统的写缓存被刷新(可能是缓存已经被写满,或者定期保存操作被执行)
这三种情况下的同步操作都会引起 Redis 主进程阻塞。
EverySec:每一秒钟同步一次
如果用户未指定appendfsync的值,则默认值为
Everysec
。每秒同步,每个写命令执行完,只是先把日志写到AOF
文件的内核缓冲区,理论上每隔1秒把缓冲区中的内容同步到磁盘,且同步操作有单独的子线程进行,因此不会阻塞主进程。Always:每执行一个命令同步一次
每个写命令执行完,立刻同步地将日志写回磁盘。此模式下同步操作是由 Redis 主进程执行的,所以在同步执行期间,主进程会被阻塞,不能接受命令请求。
三种回写策略的总结如下:
AOF重写
由于AOF是以append的形式进行写指令的追加,从而导致AOF文件会越来越庞大。当Redis服务重启时,如果过于庞大的文件必然会影响数据恢复的速度并且执行了许多无效的指令,例如针对一个KEY进行多次写操作时,我们需要的仅仅是最后一条指令。因此需要对AOF文件进行"瘦身"。"瘦身"的过程称作AOF重写(rewrite)。
虽然叫AOF重写,但是他并不是在原AOF文件的基础上进行操作得到的,而是读取Redis当前的数据状态来重新生成的。
为了避免阻塞主线程,和 AOF 日志由主进程写回不同,重写过程是由子进程执行bgrewriteaof来完成的。这样处理的最大好处是:
- 子进程进行 AOF重写期间,主进程可以继续处理命令请求
- 子进程带有主进程的数据副本,操作效率更高
那么主进程在fork子进程时,它的流程是怎样的呢?
如上图可以看出,主进程在通过 fork
系统调用生成 bgrewriteaof 子进程时,操作系统会把主进程的页表复制一份给子进程,这个页表记录着虚拟地址和物理地址映射关系,而不会复制物理内存,也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个。
这样一来,子进程就共享了父进程的物理内存数据了,这样能够节约物理内存资源,页表对应的页表项的属性会标记该物理内存的权限为只读。
不过,当父进程或者子进程在向这个内存发起写操作时,CPU 就会触发缺页中断,这个缺页中断是由于违反权限导致的,然后操作系统会在「缺页异常处理函数」里进行物理内存的复制,并重新设置其内存映射关系,将父子进程的内存读写权限设置为可读写,最后才会对内存进行写操作,这个过程被称为写时复制(Copy On Write)。
写时复制顾名思义,在发生写操作的时候,操作系统才会去复制物理内存,这样是为了防止 fork 创建子进程时,由于物理内存数据的复制时间过长而导致父进程长时间阻塞的问题。
当然,操作系统复制父进程页表的时候,父进程也是阻塞中的,不过页表的大小相比实际的物理内存小很多,所以通常复制页表的过程是比较快的。
不过,如果父进程的内存数据非常大,那自然页表也会很大,这时父进程在通过 fork 创建子进程的时候,阻塞的时间也越久。
所以,有两个阶段会导致阻塞父进程:
-
创建子进程的途中,由于要复制父进程的页表等数据结构,阻塞的时间跟页表的大小有关,页表越大,阻塞的时间也越长;
-
创建完子进程后,如果子进程或者父进程修改了共享数据,就会发生写时复制,这期间会拷贝物理内存,如果内存越大,自然阻塞的时间也越长;
触发重写机制后,主进程就会创建重写 AOF 的子进程,此时父子进程共享物理内存,重写子进程只会对这个内存进行只读,重写 AOF 子进程会读取数据库里的所有数据,并逐一把内存数据的键值对转换成一条命令,再将命令记录到重写日志(新的 AOF 文件)。
但是子进程重写过程中,主进程依然可以正常处理命令。
如果此时主进程修改了已经存在 key-value,就会发生写时复制,注意这里只会复制主进程修改的物理内存数据,没修改物理内存还是与子进程共享的。
所以如果这个阶段修改的是一个 bigkey,也就是数据量比较大的 key-value 的时候,这时复制的物理内存数据的过程就会比较耗时,有阻塞主进程的风险。
Q:当主进程进行数据修改后,如何进行AOF重写?
A:当子进程在执行AOF重写(bgrewriteaof)时, 主进程需要执行以下三个工作:
- 处理客户端的命令请求
- 将写命令追加到AOF缓冲区(aof_buf)
- 将写命令追加到AOF重写缓冲区(aof_rewrite_buf)
当子进程完成临时AOF文件重写后,它会向父进程发送一个完成信号, 父进程在接到完成信号之后, 会调用一个信号处理函数,主进程会将上面的aof_rewirte_buf缓冲区中的数据写入到子进程生成的临时AOF文件中,主进程使用临时AOF文件替换旧AOF文件,完成整个重写过程。
RDB+AOF混合持久化
RDB和AOF都是在平衡性能和数据丢失率的问题,各有优缺点。因此在Redis4.0之后,出现了RDB+AOF合体使用,该方法叫混合使用AOF日志和内存快照,也叫混合持久化。
具体开启该功能的配置为
aof-use-rdb-preamble yes
Q:混合持久化的工作流程?
A:混合持久化工作在 AOF 日志重写过程。
当开启了混合持久化时,在 AOF 重写日志时,fork出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件,然后主线程处理的操作命令会被记录在重写缓冲区里,重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。
也就是说,使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据。
混合持久化结合了RDB+AOF的优点看似完美,但是它的兼容性不好。虽然最终的文件也是.aof格式的文件,但在4.0之前版本都不识别该aof文件,同时由于前部分是RDB格式,阅读性较差。