Redis数据持久化

Series - redis

图解Redis数据持久化

Redis

保存写操作到日志的持久化方式,默认不开启(appendonly yes)

记录在AOF日志中的内容,会先在redis成功执行后再写入。这么做有两个好处:

  • 避免额外的检查开销
  • 不会阻塞当前命令的执行

但是如果命令在执行后,写入日志前服务器宕机,该条数据就有丢失的风险。

写操作执行完成再写入到AOF日志中虽不会阻塞当前操作,但有可能会阻塞下一个操作,因为命令写入日志这个动作是由主线程完成,磁盘I/O压力大时,写操作会变得很慢。

从以上两点可知,风险的发生都和写入磁盘的时机有关,所以Redis给我们提供了3种写回策略。

Redis写入AOF日志的流程:

三种写回策略实际上就是决定内核缓冲区的数据何时写入到磁盘

写回策略 写回时机 优点 缺点
Always 同步写回 数据丢失可能性最小,可靠性比较高 每次写命令都需要落盘,性能消耗大
Everysec 每秒写回 性能适中 宕机时丢失1秒内数据
No 不主动写回,由内核决定 性能最好 宕机时可能会丢失较多数据

实际上,这三种策略就是在控制fsync系统调用的发生的时机,内核将aof_buf文件的数据复制到内核缓冲区中就会进入队列,等待内核决定何时写入硬盘。

  • Always策略:每次写入AOF文件数据后,立马执行fsync();
  • Everysec策略:异步定时任务来执行fsync();
  • No策略:不执行fsync();

上面我们一直在说将数据写入AOF日志,那么AOF日志就会越来越大,如果Redis需要重启,那么数据的回复也是一个比较耗时的过程(我们都知道AOF日志中记录的是其他格式的命令,所以文件过大,命令重放就会很慢)。所以就有了AOF重写机制


重写时候,扫描当前数据库中的所有键值对,将每一个键值对用一条命令记录到新的AOF文件,全部记录后再替换。很容易想到,如果之前我们对一个键值对中的值执行了更新操作,那么新的AOF文件中就只会记录最新的键值对对应的命令,之前的更新操作不会再记录,这样一来AOF文件就会压缩。但是仔细一想,重写的过程是比较耗时的,所以需要主进程fork一个子进程来bgrewriteaof完成这个操作。


既然需要fork子进程,那fork时就一定会阻塞主进程,虽然fork会采用写时复制机制,但是fork子进程也需要拷贝必要的数据结构(内存页表等),这个拷贝的过程也需要消耗大量CPU资源,也就是说在fork完成前主进程是阻塞的。

拷贝内存页表完成后,父子进程才指向相同的内存地址空间,但子进程并没有申请与父进程同样大小的内存,这里就会用到写时复制机制了,就是字面意思-写得时候才会复制,即拷贝真正的数据。


这里可能会有人有疑问,为什么不直接在现有的AOF日志文件中操作,而要新创建一个文件?

  • 上面我们提到,会有子进程来完成AOF文件的写入,如果复用AOF文件,那么必然产生父子进程竞争问题,影响父进程性能。
  • 如果我们在重写时失败,那么可能会造成原来的AOF文件也被污染而无法使用。用新的AOF文件,即使失败,直接删除即可。

前面我们提到AOF重写是子进程bgrewriteaof来完成的,既然是子进程,那么就不会影响主进程的操作,也不会阻塞主进程。那为什么使用子进程而不是子线程呢?

设想如果我们使用了子线程,在修改共享内存数据的时候需要加锁来保证安全,可想而知性能就会降低。如果使用子进程,虽然父子进程也会共享内存数据,但是这个共享的内存是以只读的方式,就是说当父子进程任一方修改了数据,就会发生写时复制,这样父子进程就拥有了独立的数据副本,不用加锁来保证数据安全。

在Linux中,调用fork系统调用创建子进程时候,并不会复制父进程的内存页,而是与父进程共用相同的内存页,只有当父子进程的任一方对内存页做修改时才会进行复制,这就是写时复制。

1、创建子进程时,父进程的虚拟内存与物理内存关系会复制一份给子进程,并把内存设置为只读。这里父子进程共享内存数据,这样做有两个好处:

  • 加速子进程的创建
  • 减少进程对物理内存的占用

这里把内存设置为只读,是为了当对内存进行写操作时,会触发缺页异常(无写权限,无法获取数据),这样内核在缺页异常处理函数中就可以进行内存页的复制。

2、当父进程或子进程对内存数据做出修改时候,就会触发写时复制。这里会复制内存页,并重新设置映射关系,同时将父子进程的虚拟页设置为可读写,即父子进程拥有了各自的虚拟内存与对应的物理内存。


话说回来,让我们继续看看AOF重写。

上面已经说过,子进程重写过程中主进程会正常处理命令,如果此时主进程修改了已存在的键值对就会发生写时复制。这里值得注意的地方是写时复制机制此时只会复制主进程修改的物理内存数据,没有修改物理内存子进程还是会和父进程共享。这里就能提出一个问题,修改数据后,父子进程的内存数据就不一样了,这可咋整呢?


Redis设置了一个AOF重写缓冲区来解决这个问题,这个缓冲区在创建bgrewriteaof子进程后开始使用,即重写AOF时,Redis成功执行完一个命令后会把这个命令写到AOF缓冲区AOF重写缓冲区

子进程完成AOF重写后,会向主进程发送一条信号(异步),主进程收到该信号以后,就会将AOF重写缓冲区的内容追加到新的AOF文件中,然后用新的AOF文件覆盖当前AOF文件。至此,完成AOF重写操作。


另外,如果此时主进程修改的是一个bigkey(value很大,不是key很大),那么父进程在申请内存时阻塞的风险就会提高,复制物理内存也比较耗时,有阻塞主进程的风险,

整个AOF持久化机制中,有哪些操作可能会阻塞主进程?

  • 命令执行完,写入AOF日志操作完成前。写入AOF日志这个操作是主进程完成的,如果此时磁盘写压力大,那么在写入该命令时就可能发生阻塞,导致后续的操作无法正常进行。
  • 主进程fork子进程进行AOF重写时,系统调用fork操作一定会阻塞主进程。
  • 写时复制时,父进程操作大key,重新申请大块内存时间耗时就会变长,造成父进程阻塞。

RDB(Redis Database)是Redis数据持久化的另一种方式:内存快照,即记录某一时刻内存中的数据,类似于拍照时记录某一瞬间的形象。

和AOF相比,RDB恢复数据的效率更高,因为RDB快照记录的是实际数据,恢复时直接将RDB文件读入内存就行。

快照就和自拍一样,拍照时如果自己动了,那么照片就糊了。那么对于RDB而言,它也不希望数据在‘动’。 如果在执行快照期间数据都不能修改的话,那么这肯定会给我们的业务造成很大的影响,这肯定是不能被接受的。
bgsave这一子进程是由主线程fork出来共享主线程内存数据的。如果主线程在执行快照期间是进行的读操作,那么主线程和bgsave子进程就互不影响,如果是执行写操作的话,那么这里就会利用到上面讲到的操作系统提供的写时复制机制: 数据被复制一份,生成数据的副本。bgsave子进程把这个副本数据写入RDB文件,主线程仍然可以直接修改原来的数据。

  • 虽然RDB快照是bgsave子进程执行的,但如果执行快照的间隔太短,一方面频繁的写入数据到磁盘会给磁盘带来压力,另一方面,fork子进程这一过程是会阻塞主线程的,内存越大阻塞时间越长。

那么我们可以做增量快照,即做一次全量快照后,后续只需要记录修改的数据,对修改的数据做快照,减小做快照时候的开销。不过我们要是是以写操作为主的业务或者修改的键值对很多的话,记录被修改的数据同样会带来比较大的开销。

跟AOF相比,虽然RDB的恢复速度更快,但是快照的频率如何确定却也是一个问题。频率高了开销太大,频率小了又会造成数据的丢失。所以Redis 4.0提出了一个混合使用AOF日志和RDB快照的方法,即内存快照以一定的频率执行,两次快照之间用AOF记录这期间的操作。