Redis的AOF是怎么实现的?
# 答题思路
原理:执行指令后追加写入 AOF 日志文件;
保存的数据:仅保存会影响数据的写指令;
写日志的时机:先执行命令,然后再写日志;
刷盘策略:
Always
:每执行一条指令就刷一次盘;Eversec
:每秒刷一次盘;No
:不主动刷盘,由操作系统自己完成;
AOF 重写:fork 出一个子进程完成,在这个过程中,对增量数据:
- 7.0 前:通过让主进程同时写两份 AOF 实现;
- 7.0 后:主进程直接将增量数据写到新的增量 AOF 文件,AOF 重写完毕后再合并;
# 回答话术
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 createsequence
,那么 AOF 中对应的内容如下:
*3
$3
SET
$4
name
$12
createsequence
上述命令的含义如下:
*3
表示接下来有3个参数;$3
表示接下来的参数的长度为3,SET
是命令名称;$4
表示下一个参数的长度为4,name
是 key 的名称;$12
表示下一个参数的长度为12,createsequence
是要设置的值;
# 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 的文件大小比最后一次 AOF 重写后的文件膨胀大小满足 AOF 重写的增值比例;
除此之外,你也可以可以通过 BGREWRITEAOF
命令手动触发 AOF 重写。
# 5.3. 后台重写
作为一个非常重的 IO 操作,AOF 重写会长时间的阻塞线程,因此 Reids 会通过操作系统的 fork
函数分离出一个子进程 bgrewriteaof
来完成。
使用子进程的好处在于:
- 子进程进行 AOF 重写时,主进程可以正常执行,避免阻塞;
- 由于子进程带有主进程的数据副本,因此不需要像线程通过加锁控制对数据的访问;
# 5.4. 增量数据的同步
由于 AOF 重写基于父子进程,因此也带来一个问题:当子进程进行 AOF 重写时,主进程仍然还在接受指令修改数据,因此重写的 AOF 文件数据与实时数据就可能不一致。
对此,以 7.0 版本为分界线,Redis 采用了不同的处理方式:
在 7.0 之前
在 7.0 之前,Redis 采用让主进程同时写两份 AOF 文件的方式来处理这个问题。
简单的来说,当子进程在进行 AOF 重写时,如果主进程接受了一个写指令,那么它在执行后,既要将这个指令追加到 AOF 缓冲区中,也需要将其加入 AOF 重写缓存中,相当于同时写两份文件。
当子进程完成 AOF 重写后,它会向父进程发送完成信号,此时父进程将阻塞的将 AOF 重写缓存区中的数据全部写入新的 AOF 文件中,然后使用新的 AOF 文件覆盖旧的 AOF 文件。至此, AOF 重写就完成了。
在 7.0 之后
在 7.0 之后,当开始 AOF 重写的时候,主进程直接将增量数据写到一个全新的增量 AOF 文件中,等到主进程重写完 AOF,主进程再将增量 AOF 文件与重写后的 AOF 文件合并,并替换旧的 AOF 文件。
相比起 7.0 之前,对于同一个增量命令,Redis 只需写一次即可。
# 6. 数据的恢复
当 Redis 重新启动时,它会创建一个本地的伪客户端,这个客户端将会读取 AOF 日志,并且在还原出命令后发送给 Redis 服务端,直到全部的命令都执行完毕为止。
另外,根据官网文档,如果 Redis 在写 AOF 日志的过程中宕机,或者由于磁盘已满等不可抗力最终导致 AOF 日志出错,那么当重启时,Redis 会丢弃最后一个写入失败的指定,或者如果情况更糟糕,则可以通过 redis-check-aof
工具尝试修复它。
# 7. 混合持久化
由于通过 AOF 恢复数据相对比较耗时,因此 Redis 在 4.0 以后允许通过 aof‐use‐rdb‐preamble
配置开启混合持久化。
当 AOF 重写时,它将会先生成当前时间的 RDB 快照,然后将其写入新的 AOF 文件,接着再把增量数据追加到这个新 AOF 文件中。如此一来,当 Redis 通过 AOF 文件恢复数据时,将会先加载 RDB,然后再重放后半部分的增量数据。这样就可以大幅度提高数据恢复的速度。