前言
Redis是常用的一个内存数据库,也是面试的一个高频考点。先前就曾多次使用,但至于其具体原理等并不了解,因此专程学习一下。
本次学习主要目标是Redis的持久化机制,主体参照小林coding,其他部分内容来自于朋友的一次面经。
朋友的面经——顺丰二面(JAVA)
这个是朋友上个月顺丰二面中遇到的,给我带来了极大的震撼…没想到顺丰问的都这么深入了。
RDB 基于当前 Redis 节点内存数据打快照,如何打的?主进程 fork 一个子进程负责的,会不会阻塞?哪些地方可能会阻塞?RDB 存在的问题?RDB 优势?RDB 在 redis.conf 里面的配置策略。
AOF 追加写命令,又是如何操作的?每一次追加都写磁盘吗?这样不就是高磁盘 IO 了吗?底层又是如何处理的(AOF 缓冲区、系统内核缓冲区、AOF 磁盘文件、重写、write、fsync 执行时机啥的)?AOF 文件太大了的重写机制是怎样的?重写的时候文件里面只有 写命令吗?(其实还有一部分 RDB 快照)… …
AOF 和 RDB 如何选择?为什么 Redis 官方建议都开启?
AOF机制
定义
AOF全名Append Only File
,中文直译仅附加文件
。
官方文档中给出的解释是”Append-only file (AOF) is a logging mechanism that writes to a log file on disk every write operation performed on a Redis database."
即“每次对Redis数据库执行写入操作时,都会将其写入磁盘上的日志文件。"
因此可以看出,AOF是一种对操作记录的记录。通过将命令进行记录,写入到一个文件中。当重启Redis的时候,先去读取这个文件里的命令,并且执行它,从而起到恢复缓存数据的效果。
但是他仅记录了写操作,不记录读操作,因为这样是没有意义的。
执行流程
总的执行流程如图所示
将命令记录到AOF日志中,是发生在执行写操作命令之后的。这样有两个好处:
1、避免额外的检查开销。如果命令错误,就不必进行记录了。否则还需要对每次记录的命令进行语法检查,造成了额外的开销。
2、不会阻塞当前写操作命令的执行。因为当写操作命令执行成功后,才会将命令记录到 AOF 日志。
但是这样带来了两个问题:
1、 如果Redis在执行后、记录前挂掉,就会丢失这一条命令记录。
2、 可能会阻碍下一条命令的执行。(因为执行命令和写入AOF都是在主进程中完成)
在记录到AOF之后,还需要将其写入硬盘。写入硬盘如果太频繁,则会因为频繁I/O影响性能。因此需要关注到“AOF日志写回硬盘的时机”。
在Redis 写入 AOF 日志的过程中,Redis 提供了 3 种写回硬盘的策略。“Always”、“Everysec”、“No”。 这三种策略对应的是缓冲区–>硬盘这一步骤。从源码来看,他们时机就算调用fsync()
函数的时机。
他们分别如下:
Always:每次执行完就写入,即每次写入 AOF 文件数据后,就执行 fsync() 函数。
Everysec:每秒钟写入一次,即创建一个异步任务来执行 fsync() 函数。
NO:从不主动写入,将写入时机留给操作系统内核决定,即永不执行 fsync() 函数。
写入越频繁,就越影响性能,但可靠性越高。反之。因此需要根据业务场景,选用三种策略。
重写机制
AOF的大小会随着使用而逐渐增大,若持续增大,重启Redis时需执行的指令就会增多,从而影响性能。因此在AOF 文件的大小超过设定的阈值时,会利用重写机制对文件进行压缩。
简单来说,重写机制是通过精简语句进行压缩文件。例如语句Set name AAA
,随后又执行了Set name BBB
。现在其名称是BBB,因此前一句指令就可以废除。
在重写过程中,是赋值一个文件进行重写,重写结束后再进行覆盖。并不是原文件上直接进行重写。因为如果 AOF 重写过程中失败了,现有的 AOF 文件就会造成污染,可能无法用于恢复使用。
因为重写机制十分耗时,所以这个过程是在后台完成的。他在后台的 子进程bgrewriteaof
中完成。注意这里用到的是子进程而不是线程。因为线程的话会共享内存,为了避免修改引发错误,需要加锁,会影响性能。而子进程的内存本身是只读的,所以无需加锁就可以保证数据安全。
父子进程的数据同步:页表复制。页表记录着虚拟地址和物理地址的映射关系,因此父子进程的虚拟空间不同,但是物理空间相同。
只有在发生写操作时,才通过“写时复制”去复制物理内存。因为页表的大小远小于内存,所以这样可以防止fork创建子进程时,阻塞过长时间。
阻塞父进程的阶段有两个:
1、创建子进程过程中,复制页表等结构。阻塞时间和页表大小有关。
2、创建子进程之后,若发生内存复制,则同样需要阻塞。
还存在问题:
1、子进程重写过程中,主进程修改了key-value,即发生复制。但如果修改的较多,复制就会比较耗时。
2、重写过程中,如果主进程修改了已经存在 key-value,此时这个 key-value 数据在子进程的内存数据就跟主进程的内存数据不一致。
因此Redis设置了AOF重写缓冲区,该缓冲区在创建子进程后开始使用。
在子进程执行期间,主进程会同时做三件事:1、执行命令 2、发到AOF缓冲区 3、发到AOF重写缓冲区
子进程在重写结束后,会像主进程发送信号。主进程收到信号后,执行1、AOF重写缓冲区中的文件追加到新的AOF文件中,使得二者所保存内容一致。2、新的AOF文件改名、覆盖现有AOF文件。
之后结束整个重写流程,主进程就可以继续像往常一样处理命令了。
简单来说就是 把重写期间的命令存在重写缓冲区,结束后再将此部分内容写过去。
总结——重写过程中,阻塞主进程 发生的时机:1、写时复制时,会阻塞。2、信号处理时,会阻塞。(3、小林coding只写了这两点,但我认为 复制页表也应该算上。)
RDB机制
RDB全名:Redis Database
基本介绍
通过save
和bgsave
指令,分别代表是主线程运行、子进程运行,前者会阻塞。
RDB全量快照,资源消耗较大,因此不能太频繁,一般至少5分钟执行一次。(AOF机制是秒级)
通过写时复制机制,开始执行时,对内存进行复制。创建子进程时候复制页表,修改内存时 再复制物理内存。
在这个过程中修改的数据,在主进程的物理内存中进行。而复制发生在后台子进程的物理内存。因此,此过程修改的数据,是无法写入到RDB中的。
极端情况——要是RDB的所有数据,在此过程中都修改了一遍,那会直接占据二倍的内存。
AOF+RDB(混合持久化)
当开启了混合持久化时,在 AOF 重写日志时,fork 出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件,然后主线程处理的操作命令会被记录在重写缓冲区里,重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。
也就是说,使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据。
对比——RDB和AOF
优缺点
RDB的优缺点:
优点:RDB持久化文件,速度比较快,而且存储的是一个二进制文件,传输起来很方便。
缺点:RDB无法保证数据的绝对安全,因为他备份间隔久。
AOF的优缺点:
优点:AOF相对RDB更加安全,一般不会有数据的丢失或者很少,官方推荐同时开启AOF和RDB。
缺点:AOF持久化的速度,相对于RDB较慢,存储的是一个文本文件,到了后期文件会比较大,传输困难。