Redis的AOF是怎么实现的?
作者:程序员马丁
在线博客:https://open8gu.com
大话面试,技术同学面试必备的八股文小册,以精彩回答应对深度问题,助力你在面试中拿个offer。
答题思路
回答话术
AOF 即 Append Only File
,它是 Redis 提供的一种持久化机制。
其原理是每当服务器执行写指令时,将命令追加到 AOF 日志文件。当 Redis 重新启动时,他会在本地启动一个伪客户端,并按顺序重新发送日志中的命令以恢复数据。
Redis 的 AOF 日志和 MySQL 的 binlog 有点像,当执行一个命令后,数据会先写入 AOF 缓冲区,再写入操作系统缓冲区,最后根据刷盘策略调用 fsync
函数将数据刷入磁盘。Redis 默认提供三种刷盘策略:Always(每个命令后都刷盘)、Everysec(每秒刷一次盘)、No(等到操作系统缓冲区满或定期刷盘)。
当 AOF 日志越来越大的时候,会触发 AOF 重写。举个例子,假如在 Redis 中对 1 递增了 99 次,那么 AOF 文件会记录一百条命令,但是实际上我们恢复数据的时候只需要一个最终值 100,中间的步骤都是不需要的。基于这个原理,在 Redis 重写的过程中,它会开启一个子进程扫描数据库,并生成一个新的 AOF 文件去替换旧的文件。这个文件将会比原本的文件精简,并且哪怕这个过程中 Redis 挂了,也不会影响已有的 AOF 文件。
不过,在子进程进行 AOF 重写的过程中,由于主进程还在不停的接受新的指令,因此它除了需要写自己的 AOF 缓冲区外,还需要将其写到 AOF 重写缓冲区中,以此实现重写过程中的增量数据同步。
问题详解
1. 保存哪些命令
在 AOF 文件中,只会保存写指令,或者更准确点说,只会保存修改数据的指令。
比如,我们依次执行了下述指令:
RPUSH list 1 2 3 4
LRANGE list 0 -1
KEYS *
LPUSH list 1
那么,最终只会保存两条:
RPUSH list 1 2 3 4
LPUSH list 1
2. 数据的保存格式
值得注意的是,在 Redis 中执行的命令并不会原模原样的保存到 AOF ,而是以一种比较特殊 $[长度] + [指令]
的格式保存。
比如,我们执行一个简单的命令 SET name open8gu
,那么 AOF 中对应的内容如下:
*3
$3
SET
$4
name
$7
open8gu
上述命令的含义如下:
*3
表示接下来有 3 个参数;$3
表示接下来的参数的长度为 3,SET
是命令名称;$4
表示下一个参数的长度为 4,name
是 Key 的名称;$7
表示下一个参数的长度为 7,open8gu
是要设置的值。
3. 写日志的时机
当 Redis 接收到一条指令的时候,它会先执行指令,然后再写 AOF 日志。
这个逻辑与我们熟悉的 MySQL 中的 binlog 不同,后者是写前日志(Write Ahead Log, WAL),即先写日志再保存数据,而 AOF 日志则是写后日志,即先保存数据再写日志。
这种处理方式的优点是:
- 可以确保写入 AOF 日志指令都是没有错误的可执行的指令,避免写日志时还需要进行额外的语法/类型检查,或者等出错后回滚日志。
- 不因为写日志而阻塞当前指令的执行。
不过对应的缺点也很明显:
- 如果执行完指令实例突然挂了,那 AOF 日志中就不会记录这条指令。
- 由于 Redis 的大多数命令都由单个线程执行,因此可能因为写日志而阻塞后一条指令的执行。
4. 日志的刷盘策略
实际上,将指令数据写入磁盘的时候,并不是一步完成的:
- 当执行了写指令后,数据首先被写入 Redis 自己的 AOF 缓冲区;
- 随后,Redis 会调用操作系统的
write
函数,将数据从 AOF 缓冲区写入操作系统缓冲区; - 最后,再由 Redis 调用
fsync
函数或操作系统自己刷盘,让内核缓冲区中的数据真正写入磁盘。
第三步即我们通常说的“日志刷盘”。在日志真正的刷到磁盘之前,数据仍然仅保存在内存里,此时一旦服务器宕机,数据将会永久性的丢失。因此,何时刷盘是整个持久化流程的关键点。
在 MySQL 中,我们可以通过 binlog_sync
来指定 binlog 的刷盘策略,而在 Redis 中,我们可以通过 appendfsync
配置项指定 AOF 日志的的刷盘策略:
AOF_FSYNC_NO
:AOF 缓冲区有数据时(即执行一个命令后),调用write
函数写入操作系统缓冲区,然后操作系统定期(在 Linux 中通常是 30 秒)或缓冲区满后再自动写入磁盘。AOF_FSYNC_EVERYSEC
:AOF 缓冲区有数据时(即执行一个命令后),调用write
函数写入操作系统缓冲区,然后每一秒钟调用一次fsync
将数据写入磁盘。AOF_FSYNC_ALWAYS
:AOF 缓冲区有数据时(即执行一个命令后),立刻调用fsync
将数据写入磁盘。
这三种配置方式各有优劣,它们会很大程度上的影响 Redis 的性能:
指令 | 时机 | 性能 | 宕机时丢失的数据 |
---|---|---|---|
AOF_FSYNC_NO | 不主动刷盘,由操作系统自己决定刷盘时机 | 高 | 所有未写入磁盘的数据 |
AOF_FSYNC_EVERYSEC | 每秒保存一次 | 中 | 一秒内的数据 |
AOF_FSYNC_ALWAYS | 每个命令执行后保存一次 | 低 | 一条指令的数据 |
总的来说,核心问题在于如何取舍可靠性与性能:
- 如果你的系统对数据可靠性要求极高,不允许数据丢失,那么你应该选择
ALWAYS
。 - 如果你的系统更在乎性能,而不在意丢失一些数据,那么你可以选择
NO
。 - 如果你想要在两者间取得平衡,那么你可以选择
EVERSEC
。
5. AOF 重写
5.1. 实现原理
随着写入操作的进行,AOF 文件会变得越来越大,而这其中的大多数数据是没必要保存的。
比如,你把 1 递增到 100,那么最终 AOF 会记录这一过程中的全部 100 条指令。然而实际上我们只需要最终的值 100 即可。
因此,AOF 提供了一种重写机制,即当 AOF 文件膨胀到一定程度时,Redis 将直接重新扫描当前数据库中的数据,然后把它们重写到一个新 AOF 文件中,并替换旧的 AOF 文件,这个新的 AOF 文件会比原本的文件更小。
在这个过程中,Redis 实际上完全不会重新读取原有的 AOF 文件。
5.2. 触发条件
在 2.4 版本以后,当你开启 AOF 功能,Redis 会在满足下述三个条件的时候自动触发 AOF 重写:
- 当前没有正在执行的 AOF 重写或 RDB 生成操作。
- 当前的 AOF 文件大于
server.aof_rewrite_min_size
配置。 - 当前 AOF 的文件大小增幅达到设置的比例(比如比上一次重写后的文件大了 50%)。
除此之外,你也可以可以通过 BGREWRITEAOF
命令手动触发 AOF 重写。
5.3. 后台重写
作为一个非常重的 IO 操作,AOF 重写会长时间的阻塞线程,因此 Reids 会通过操作系统的 fork
函数分离出一个子进程 bgrewriteaof
来完成。
使用子进程的好处在于:
- 子进程进行 AOF 重写时,主进程可以正常执行,避免阻塞。
- 由于子进程带有主进程的数据副本,因此不需要像线程通过加锁控制对数据的访问。
5.4. 增量数据的同步
由于 AOF 重写基于父子进程,因此也带来一个问题:当子进程进行 AOF 重写时,主进程仍然还在接受指令修改数据,因此重写的 AOF 文件数据与实时数据就可能不一致。
对此,以 7.0 版本为分界线,Redis 采用了不同的处理方式:
5.4.1. 7.0 版本之前
在 7.0 之前,Redis 采用让主进程同时写两份 AOF 文件的方式来处理这个问题。
简单的来说,当子进程在进行 AOF 重写时,如果主进程接受了一个写指令,那么它在执行后,既要将这个指令追加到 AOF 缓冲区中,也需要将其加入 AOF 重写缓存中,相当于同时写两份文件。
当子进程完成 AOF 重写后,它会向父进程发送完成信号,此时父进程将阻塞的将 AOF 重写缓存区中的数据全部写入新的 AOF 文件中,然后使用新的 AOF 文件覆盖旧的 AOF 文件。至此, AOF 重写就完成了。
5.4.2. 7.0 版本之后
在 7.0 之后,当开始 AOF 重写的时候,主进程直接将增量数据写到一个全新的增量 AOF 文件中,等到子进程重写完 AOF,主进程再将增量 AOF 文件与重写后的 AOF 文件合并,并替换旧的 AOF 文件。
相比起 7.0 之前,对于同一个增量命令,主进程主需要往增量 AOF 文件里面写一次即可,不必再向子进程正在重写的 AOF 文件里面另外再写一条数据了。
需要注意的是,虽然我们称新的 AOF 文件为“增量 AOF”文件,不过对于主进程来说两个 AOF 文件没啥区别,只是从某一条命令开始换了一个文件写罢了,这条命令之前的数据全部在旧 AOF 文件,而这条命令及以后的数据都在新 AOF 文件里,这个过程依然受刷盘策略控制。
6. 数据的恢复
当 Redis 重新启动时,它会创建一个本地的伪客户端,这个客户端将会读取 AOF 日志,并且在还原出命令后发送给 Redis 服务端,直到全部的命令都执行完毕为止。
另外,根据官网文档,如果 Redis 在写 AOF 日志的过程中宕机,或者由于磁盘已满等不可抗力最终导致 AOF 日志出错,那么当重启时,Redis 会丢弃最后一个写入失败的指定,或者如果情况更糟糕,则可以通过 redis-check-aof
工具尝试修复它。