一、引言
Redis 是一种内存数据库,将数据保存在内存中,读写效率要比传统的将数据保存在磁盘上的数据库要快很多。但是一旦进程退出,Redis 的数据就会丢失。
为了解决这个问题,Redis 提供了 RDB 和 AOF 两种持久化方案,将内存中的数据保存到磁盘中,避免数据丢失。RDB的介绍在这篇文章中《Redis RDB 持久化详解》,今天我们来看一下 AOF 相关的原理。
AOF( append only file )持久化以独立日志的方式记录每次写命令,并在 Redis 重启时在重新执行 AOF 文件中的命令以达到恢复数据的目的。AOF 的主要作用是解决数据持久化的实时性。
二、RDB 和 AOF对比
antirez 在《Redis 持久化解密》一文中讲述了 RDB 和 AOF 各自的优缺点:
- RDB 是一个紧凑压缩的二进制文件,代表 Redis 在某个时间点上的数据备份。非常适合备份,全量复制等场景。比如每6小时执行 bgsave 备份,并把 RDB 文件拷贝到远程机器或者文件系统中,用于灾难恢复。
- Redis 加载 RDB 恢复数据远远快于 AOF 的方式
- RDB 方式数据没办法做到实时持久化,而 AOF 方式可以做到。
下面,我们就来了解一下 AOF 是如何做到实时持久化的。
三、AOF文件同步的三种机制
(1)每修改同步always:同步持久化,每次发生数据变更会被立即记录到磁盘,性能较差但数据完整性比较好
(2)每秒同步everysec:异步操作,每秒记录,如果一秒内宕机,有数据丢失
(3)不同no:从不同步
四、AOF 持久化流程
如上图所示,AOF 持久化功能的实现可以分为命令追加( append )、文件写入( write )、文件同步( sync )、文件重写(rewrite)和重启加载(load)。其流程如下:
- 所有的写命令会追加到 AOF 缓冲中。
- AOF 缓冲区根据对应的策略向硬盘进行同步操作。
- 随着 AOF 文件越来越大,需要定期对 AOF 文件进行重写,达到压缩的目的。
- 当 Redis 重启时,可以加载 AOF 文件进行数据恢复。
五、命令追加
当 AOF 持久化功能处于打开状态时,Redis 在执行完一个写命令之后,会以协议格式(也就是RESP协议,即 Redis 客户端和服务器交互的通信协议 )将被执行的写命令追加到 Redis 服务端维护的 AOF 缓冲区末尾。
比如说 SET mykey myvalue 这条命令就以如下格式记录到 AOF 缓冲中。
1 "*3
$3
SET
$5
mykey
$7
myvalue
"
Redis 协议格式本文不再赘述,,AOF之所以直接采用文本协议格式,是因为所有写入命令都要进行追加操作,直接采用协议格式,避免了二次处理开销。
六、文件写入和同步
6.1 流程分析
Redis 每次结束一个事件循环之前,它都会调用flushAppendOnlyFile函数,判断是否需要将 AOF 缓存区中的内容写入和同步到 AOF 文件中。 这个函数执行以下两个工作:
-
- WRITE:根据条件,将 aof_buf 中的缓存写入到 AOF 文件。
-
- SAVE:根据条件,调用 fsync 或 fdatasync 函数,将 AOF 文件保存到磁盘中。
两个步骤都需要根据一定的条件来执行, 而这些条件由 AOF 所使用的保存模式来决定, 下面就来介绍 AOF 所使用的三种保存模式, 以及在这些模式下, 步骤 WRITE 和 SAVE 的调用条件。
flushAppendOnlyFile函数的行为由 redis.conf 配置中的appendfsync选项的值来决定。该选项有三个可选值,分别是 always
、 everysec
和 no
:
always
:Redis 在每个事件循环都要将 AOF 缓冲区中的所有内容写入到 AOF 文件,并且同步 AOF 文件,所以always
的效率是appendfsync
选项三个值当中最差的一个,但从安全性来说,也是最安全的。当发生故障停机时,AOF 持久化也只会丢失一个事件循环中所产生的命令数据。
在这种模式下,每次执行完一个命令之后, WRITE 和 SAVE 都会被执行。
另外,因为 SAVE 是由 Redis 主进程执行的,所以在 SAVE 执行期间,主进程会被阻塞,不能接受命令请求。
everysec
:Redis 在每个事件循环都要将 AOF 缓冲区中的所有内容写入到 AOF 文件中,并且每隔一秒就要在子进程中对 AOF 文件进行一次同步。从效率上看,该模式足够快。当发生故障停机时,只会丢失一秒钟的命令数据。
在这种模式中, SAVE 原则上每隔一秒钟就会执行一次, 因为 SAVE 操作是由后台子线程调用的, 所以它不会引起服务器主进程阻塞。
注意, 在上一句的说明里面使用了词语“原则上”, 在实际运行中, 程序在这种模式下对 fsync 或 fdatasync 的调用并不是每秒一次, 它和调用 flushAppendOnlyFile 函数时 Redis 所处的状态有关。
每当 flushAppendOnlyFile 函数被调用时, 可能会出现以下四种情况:
子线程正在执行 SAVE ,并且:
这个 SAVE 的执行时间未超过 2 秒,那么程序直接返回,并不执行 WRITE 或新的 SAVE 。
这个 SAVE 已经执行超过 2 秒,那么程序执行 WRITE ,但不执行新的 SAVE 。注意,因为这时 WRITE 的写入必须等待子线程先完成(旧的) SAVE ,因此这里 WRITE 会比平时阻塞更长时间。
子线程没有在执行 SAVE ,并且:
上次成功执行 SAVE 距今不超过 1 秒,那么程序执行 WRITE ,但不执行 SAVE 。
上次成功执行 SAVE 距今已经超过 1 秒,那么程序执行 WRITE 和 SAVE 。
可以用流程图表示这四种情况:
根据以上说明可以知道, 在“每一秒钟保存一次”模式下, 如果在情况 1 中发生故障停机, 那么用户最多损失小于 2 秒内所产生的所有数据。
如果在情况 2 中发生故障停机, 那么用户损失的数据是可以超过 2 秒的。
Redis 官网上所说的, AOF 在“每一秒钟保存一次”时发生故障, 只丢失 1 秒钟数据的说法, 实际上并不准确。
no
:Redis 在每一个事件循环都要将 AOF 缓冲区中的所有内容写入到 AOF 文件。而 AOF 文件的同步由操作系统控制。这种模式下速度最快,但是同步的时间间隔较长,出现故障时可能会丢失较多数据。
在这种模式下, 每次调用 flushAppendOnlyFile 函数, WRITE 都会被执行, 但 SAVE 会被略过。在这种模式下, SAVE 只会在以下任意一种情况中被执行:
-
- Redis 被关闭
- AOF 功能被关闭
- 系统的写缓存被刷新(可能是缓存已经被写满,或者定期保存操作被执行)
这三种情况下的 SAVE 操作都会引起 Redis 主进程阻塞。
Linux 系统下write操作会触发延迟写( delayed write )机制。Linux 在内核提供页缓存区用来提供硬盘 IO 性能。 write
操作在写入系统缓冲区之后直接返回。同步硬盘操作依赖于系统调度机制,例如:缓冲区页空间写满或者达到特定时间周期。同步文件之前,如果此时系统故障宕机,缓冲区内数据将丢失。
而fsync针对单个文件操作,对其进行强制硬盘同步,fsync将阻塞直到写入磁盘完成后返回,保证了数据持久化。
appendfsync
的三个值代表着三种不同的调用 fsync
的策略。调用 fsync
周期越频繁,读写效率就越差,但是相应的安全性越高,发生宕机时丢失的数据越少。
有关 Linux 的I/O和各个系统调用的作用如下图所示。具体内容可以查看《聊聊 Linux I/O》一文。
6.2 AOF 保存模式对性能和安全性的影响
对于三种 AOF 保存模式, 它们对服务器主进程的阻塞情况如下:
-
- 不保存(AOF_FSYNC_NO):写入和保存都由主进程执行,两个操作都会阻塞主进程。
- 每一秒钟保存一次(AOF_FSYNC_EVERYSEC):写入操作由主进程执行,阻塞主进程。保存操作由子线程执行,不直接阻塞主进程,但保存操作完成的快慢会影响写入操作的阻塞时长。
- 每执行一个命令保存一次(AOF_FSYNC_ALWAYS):和模式 1 一样。
因为阻塞操作会让 Redis 主进程无法持续处理请求, 所以一般说来, 阻塞操作执行得越少、完成得越快, Redis 的性能就越好。
模式 1 的保存操作只会在AOF 关闭或 Redis 关闭时执行, 或者由操作系统触发, 在一般情况下, 这种模式只需要为写入阻塞, 因此它的写入性能要比后面两种模式要高, 当然, 这种性能的提高是以降低安全性为代价的: 在这种模式下, 如果运行的中途发生停机, 那么丢失数据的数量由操作系统的缓存冲洗策略决定。
模式 2 在性能方面要优于模式 3 , 并且在通常情况下, 这种模式最多丢失不多于 2 秒的数据, 所以它的安全性要高于模式 1 , 这是一种兼顾性能和安全性的保存方案。
模式 3 的安全性是最高的, 但性能也是最差的, 因为服务器必须阻塞直到命令信息被写入并保存到磁盘之后, 才能继续处理请求。
综合起来,三种 AOF 模式的操作特性可以总结如下:
七、AOF 数据恢复
7.1 流程图
AOF 文件里边包含了重建 Redis 数据所需的所有写命令,所以 Redis 只要读入并重新执行一遍 AOF 文件里边保存的写命令,就可以还原 Redis 关闭之前的状态。
7.2 流程小结
Redis 读取 AOF 文件并且还原数据库状态的详细步骤如下:
- 创建一个不带网络连接的的伪客户端( fake client),因为Redis 的命令只能在客户端上下文中执行,而载入 AOF 文件时所使用的的命令直接来源于 AOF 文件而不是网络连接,所以服务器使用了一个没有网络连接的伪客户端来执行 AOF 文件保存的写命令,伪客户端执行命令的效果和带网络连接的客户端执行命令的效果完全一样的。
- 从 AOF 文件中分析并取出一条写命令。
- 使用伪客户端执行被读出的写命令。
- 一直执行步骤 2 和步骤3,直到 AOF 文件中的所有写命令都被处理完毕为止。
当完成以上步骤之后,AOF 文件所保存的数据库状态就会被完整还原出来。
八、AOF 重写
8.1 为什么需要进行重写
- AOF 持久化是通过保存被执行的写命令来记录数据库状态的,所以AOF文件的大小随着时间的流逝一定会越来越大;影响包括但不限于:对于Redis服务器,计算机的存储压力;AOF还原出数据库状态的时间增加;
- 为了解决AOF文件体积膨胀的问题,Redis提供了AOF重写功能:Redis服务器可以创建一个新的AOF文件来替代现有的AOF文件,新旧两个文件所保存的数据库状态是相同的,但是新的AOF文件不会包含任何浪费空间的冗余命令,通常体积会较旧AOF文件小很多。
8.2 AOF重写实现原理
如上图所示,重写前要记录名为 list
的键的状态,AOF 文件要保存五条命令,而重写后,则只需要保存一条命令。
AOF 文件重写并不需要对现有的 AOF 文件进行任何读取、分析或者写入操作,而是通过读取服务器当前的数据库状态来实现的。首先从数据库中读取键现在的值,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令,这就是 AOF 重写功能的实现原理。
在实际过程中,为了避免在执行命令时造成客户端输入缓冲区溢出,AOF 重写在处理列表、哈希表、集合和有序集合这四种可能会带有多个元素的键时,会先检查键所包含的元素数量,如果数量超过 REDISAOFREWRITEITEMSPER_CMD ( 一般为64)常量,则使用多条命令记录该键的值,而不是一条命令。
rewrite的触发机制主要有一下三个:
- 手动调用 bgrewriteaof 命令,如果当前有正在运行的 rewrite 子进程,则本次rewrite 会推迟执行,否则,直接触发一次 rewrite。
- 通过配置指令手动开启 AOF 功能,如果没有 RDB 子进程的情况下,会触发一次 rewrite,将当前数据库中的数据写入 rewrite 文件。
- 在 Redis 定时器中,如果有需要退出执行的 rewrite 并且没有正在运行的 RDB 或者 rewrite 子进程时,触发一次或者AOF文件大小已经到达配置的 rewrite 条件也会自动触发一次。
8.4 AOF 后台重写
- aof_rewrite函数可以创建新的AOF文件,但是这个函数会进行大量的写入操作,所以调用这个函数的线程将被长时间的阻塞,因为Redis服务器使用单线程来处理命令请求;所以如果直接是服务器进程调用AOF_REWRITE函数的话,那么重写AOF期间,服务器将无法处理客户端发送来的命令请求;
- Redis不希望AOF重写会造成服务器无法处理请求,所以Redis决定将AOF重写程序放到子进程(后台)里执行。这样处理的最大好处是:
- 子进程进行AOF重写期间,主进程可以继续处理命令请求;
- 子进程带有主进程的数据副本,使用子进程而不是线程,可以避免在锁的情况下,保证数据的安全性
8.5 使用子进程进行AOF重写的问题
- 子进程在进行AOF重写期间,服务器进程还要继续处理命令请求,而新的命令可能对现有的数据进行修改,这会让当前数据库的数据和重写后的AOF文件中的数据不一致。
8.6 如何修正
为了解决这种数据不一致的问题,Redis增加了一个AOF重写缓存区,这个缓存在fork出子进程之后开始启用,Redis服务器主进程在执行完写命令之后,会同时将这个写命令追加到AOF缓冲区和AOF重写缓冲区。
8.7 在AOF重写期间主进程需要完成的工作
即子进程在执行AOF重写时,主进程需要执行以下三个工作:
- 执行client发来的命令请求;
- 将写命令追加到AOF缓存区,也就是加入现有的AOF文件中;
- 将写命令追加到AOF重写缓存区中。
8.8 AOF缓存区的作用
可以保证:
- AOF缓冲区的内容会定期被写入和同步到AOF文件中,对现有的AOF文件的处理工作会正常进行
- 从创建子进程开始,服务器执行的所有写操作都会被记录到AOF重写缓冲区中;
8.9 完成AOF重写之后
当子进程完成对AOF文件重写之后,它会向父进程发送一个完成信号,父进程接到该完成信号之后,会调用一个信号处理函数,该函数完成以下工作:
- 将AOF重写缓存中的内容全部写入到新的AOF文件中;这个时候新的AOF文件所保存的数据库状态和服务器当前的数据库状态一致;
- 对新的AOF文件进行改名,原子的覆盖原有的AOF文件;完成新旧两个AOF文件的替换。
当这个信号处理函数执行完毕之后,主进程就可以继续像往常一样接收命令请求了。在整个 AOF 后台重写过程中,只有fork子进程和信号处理函数执行时会对 Redis 主进程造成阻塞,在其他时候,AOF 后台重写都不会阻塞主进程。
8.10 触发AOF后台重写的条件
- AOF重写可以由用户通过调用BGREWRITEAOF手动触发。
- 服务器在AOF功能开启的情况下,会维持以下三个变量:
- 记录当前AOF文件大小的变量aof_current_size。
- 记录最后一次AOF重写之后,AOF文件大小的变量aof_rewrite_base_size。
- 增长百分比变量aof_rewrite_perc。
- 每次当serverCron(服务器周期性操作函数)函数执行时,它会检查以下条件是否全部满足,如果全部满足的话,就触发自动的AOF重写操作:
- 没有BGSAVE命令(RDB持久化)/AOF持久化在执行;
- 没有BGREWRITEAOF在进行;
- 当前AOF文件大小要大于server.aof_rewrite_min_size(默认为64MB),或者在redis.conf配置了auto-aof-rewrite-min-size大小;
- 当前AOF文件大小和最后一次重写后的大小之间的比率等于或者等于指定的增长百分比(在配置文件设置了auto-aof-rewrite-percentage参数,不设置默认为100%)
如果前面三个条件都满足,并且当前AOF文件大小比最后一次AOF重写时的大小要大于指定的百分比,那么触发自动AOF重写。
九、参考文章
https://baijiahao.baidu.com/s?id=1654694618189745916&wfr=spider&for=pc
https://www.cnblogs.com/remcarpediem/p/11644722.html
https://blog.csdn.net/hezhiqiang1314/article/details/69396887#aof-%E9%87%8D%E5%86%99
https://www.jianshu.com/p/1e34fdc51e3b