AOF日志
Redis在每次执行命令之后,再将写操作指令写入磁盘文件
- 这样的先后顺序有什么好处?
一方面可以避免额外的开销,如果先写日志再执行命令,可能将含有语法错误的命令写入日志,后面运行日志的内容时,就需要检查语法;
另一方面,不会阻塞写命令的执行。如果一旦写日志,就会涉及到磁盘IO,可能会阻塞主进程执行写指令
- AOF潜在的风险?
一方面数据可能会丢失。因为执行了写命令后,写指令还没来得及写入磁盘,Redis就宕机了,就会丢失内存中的数据;
另一方面,会给下一个写指令带来阻塞的风险。因为执行写指令的时候,可能还在写入前面的写指令到AOF日志当中,这时就会出现写命令阻塞的风险。
而这两个风险,其实都与“AOF日志写回磁盘的时机”有关
三种写回策略
AOF日志写回磁盘的时机对应了三种策略
- Always:写指令写入aof_buf后调用write()写入内核缓冲区page cahce之后马上执行fysnc()同步刷新到磁盘
- EverySec:写指令写入aof_buf后,执行wirte()写入操作系统的内核缓冲区page cache,之后再每隔1s调用fsync()将写指令同步到磁盘
- No:写指令写入aof_buf后,执行write()写入操作系统的内核缓冲区page cahce,,然后交由操作系统决定何时调用fsync()
- 三种策略都不能同时兼顾“高可靠”和“高性能”,高可靠是说保证数据不会丢失,高性能是说不会发生主进程阻塞。
对于Always策略,每次执行完写指令就将其写入磁盘,保证了数据不会丢失,但是使得主进程性能下降
对于No策略,写指令写入操作系统的内核缓冲区后就不管了,主进程的性能较好,但是服务器就会导致还没来得及刷盘的数据丢失
对于EverySec可以说是比较折中的策略了,每次执行完写指令后写入操作系统的内核缓冲区,但是不是就不管了,而是会每隔1s调用fsync()进行刷盘,避免了Always策略的大量性能开销,也比No策略更能避免数据的丢失
AOF重写
大量的写指令写入AOF文件,会使得文件过大,而且一个键值对可能被多次修改,造成数据冗余,因此,当AOF文件达到一定大小后,就会触发AOF重写
AOF重写时,会读取当前数据库中的所有键值对,然后将每一个键值对用一条命令记录到“新的AOF文件”中。创建一个新的AOF文件是因为,如果修改旧的AOF文件,一旦重写失败,就会污染原来的AOF文件,造成Redis重启后数据无法恢复的情况。这样,重写完成后,就会用新的AOF文件覆盖旧的AOF文件。
AOF后台重写
AOF重写一般是由后台子进程bgrewriteof来完成的,这样做有两个好处。
一方面,AOF重写操作是很耗时的,可以避免阻塞主进程的读写操作
另一方面,采用子进程而不是子线程,可以避免多线程共享内存时,为了安全而加锁带来的性能降低。而采用子进程,子进程可以共享父进程的数据副本
父进程会fork出一个子进程,并将自己的页表复制一份给子进程,复制页表过程中,根据父进程页表对应的物理空间的大小,复制的页表大小也不同,父进程阻塞的时间也不同,不过比起直接复制物理内存来说,还是很快的(这也是写时复制技术的好处所在)。
值得注意的是,这个时候子进程和父进程的页表中页表项的属性会标记该页表项对应的物理内存只读。一旦父进程或者子进程对共享数据进行了写操作,就会触发操作系统的“写时复制”。具体来说,当CPU执行写操作时,发现违反了权限,就会触发CPU的写保护中断,操作系统会在写保护中断函数中进行物理内存的复制。在AOF重写时,如果父进程对已经存在的键值对进行修改,就会复制父进程(主进程)修改的物理内存数据,将父子进程的内存读写权限设置为可读写,然后才进行父进程对应内存的写操作**,而没有修改的物理内存数据,还是一起共享的,**如果这时修改的是一个大key,就会造成父进程的阻塞。
- AOF重写过程中,父进程修改了已经存在的键值对,父子进程的内存数据就不一致了,怎么办呢?
其实,在AOF重写的过程中,Redis执行完一条写命令后,会同时将它放到AOF缓冲区和AOFC重写缓冲区,子进程完成AOF的重写操作,就是将其内存中的键值对一个个转换成一条条命令后,写入新的AOF文件中,会向主进程发送一个异步信号,主进程接收到信号后就会调用一个信号处理函数,其中,会做两件事,一个是将AOF缓冲区中的内容追加到新的AOF文件中(使得新的AOF保持的是最新的数据库状态),然后再将新的AOF文件改名覆盖原来的AOF文件。不过,信号处理函数执行过程中也会对主进程造成阻塞