append only file追加方式持久化AOF_Redis

Redis 持久化方式:AOF

一般说到 Redis 的使用业务场景,很多人首先想到的一定会是把 Redis 当作缓存,它会将后端数据库中的数据存储在内存中,然后直接从内存中读取数据,所以响应速度会很快。

这确实是 Redis 的最常见使用场景。但是,还有一个不容忽视的问题:如果服务器一旦崩溃,内存中的所有数据都将会丢失。

第一个明显能想到的解决方案是从后端数据库恢复这些数据。然而,这个方法会存在两个问题:

  • 频繁访问数据库会给数据库带来访问压力。
  • 这些数据是从慢速数据库中读取的。性能肯定不如直接从 Redis 读取,导致使用这些数据的应用程序响应速度较慢。

因此,对于 Redis 来说,能自身实现数据持久化并避免从后端数据库恢复至关重要。

持久化方法

Redis 中主要提供两种数据持久化方式:

  • AOF(Append Only File,追加文件):实时追加命令的日志的文件。
  • RDB(Redis Database Backup file,Redis 数据库备份文件):生成数据快照文件。

本文我们将重点学习 AOF 的实现。

AOF 日志如何实现?

说到日志,我们比较熟悉的是数据库的预写日志(WAL)。即在真正写入数据之前,首先将修改的数据记录到日志文件中,以便发生故障时进行恢复。

然而AOF日志却恰恰相反。这是一个后写日志。即Redis先执行命令,将数据写入内存,然后记录日志,如下图所示:

另外,在Redis中,默认情况下并没有启用AOF持久化功能。我们需要修改redis.conf 配置文件中的以下参数:

// redis.conf
appendonly      yes                 //表示是否启用 AOF。 默认为 no。
appendfilename  "appendonly.aof"    //AOF 文件的名称。

Redis AOF 操作流程

那么为什么AOF先执行命令后才记录日志呢?要回答这个问题,我们首先要知道AOF中记录了什么。

传统数据库的日志,例如MySQL的 redo 日志,记录的是修改的数据。而AOF记录了Redis接收到的每条命令,并且这些命令以文本形式保存。

我们以Redis收到命令set testkey testvalue后记录的日志为例,来看看AOF日志的内容。

其中, *3表示当前命令分为三部分,每部分都以$+digital开头,后跟具体的命令、键或值。这里的“digital”表示该部分的命令、键或值有多少个字节。例如$3 set表示这部分有3个字节,是set命令。 $7 testkey表示密钥的字符串长度。

Redis AOF 日志内容

不过,为了避免额外的检查开销,Redis 在 AOF 中记录日志时,不会先对这些命令进行语法检查。所以,如果先记录日志再执行命令,日志中可能会记录错误的命令,Redis使用日志恢复数据时可能会出现错误。

write-after 日志记录方法是让系统先执行命令。只有命令执行成功才会记录在日志中。否则系统会直接向客户端报错。因此,Redis使用write-after日志记录方式的一大优点就是可以避免记录错误的命令。

另外,AOF还有一个优点:它在命令执行后记录日志,因此不会阻塞当前的写操作

但是,AOF 也有两个潜在的风险。

  1. 如果刚刚执行了一条命令,在记录日志之前系统就崩溃了,那么就有丢失这条命令和相应数据的风险。如果此时使用Redis作为缓存,则可以从后端数据库读回数据进行恢复。但是如果直接使用Redis作为数据库,此时由于命令没有记录在日志中,因此无法使用日志进行恢复
  2. AOF虽然避免了阻塞当前命令,但可能会给下一步操作带来阻塞风险。这是因为AOF日志也是在主线程中执行的。如果日志文件写入磁盘时磁盘写入压力较高,会导致磁盘写入缓慢,进而无法执行后续操作

仔细分析一下,可以发现这两个风险都与AOF日志写回磁盘的时机有关。这意味着,如果我们能够控制执行写入命令后将AOF日志写回磁盘的时机,那么这两个风险就可以消除。

而这,可以通过设置 AOF 的回写策略来实现。

三种回写策略

其实对于这个问题,AOF机制为我们提供了三种选择,就是AOF配置项appendfsync的三个可选值。

  • always,同步回写:每次执行写命令后,日志立即同步写回磁盘。
  • everysec,每秒回写:每次执行写命令后,首先只将日志写入 AOF 文件的内存缓冲区,每秒将缓冲区中的内容写入磁盘。
  • no,由操作系统控制的回写:每次执行写命令后,仅先将日志写入AOF文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。

为了避免阻塞主线程和减少数据丢失,这三种回写策略都无法同时实现。我们来分析一下原因。

  • “同步回写”基本上可以避免数据丢失,但是它在每次写命令后都有一个缓慢的磁盘写操作,这不可避免地影响了主线程的性能。
  • 虽然“操作系统控制回写”可以在写入缓冲区后继续执行后续命令,但磁盘写入的时机已经不再掌握在Redis手中。只要AOF记录不写回磁盘,一旦系统崩溃,相应的数据就会丢失。
  • “每秒回写”采用每秒回写一次的频率,避免了“同步回写”的性能开销。虽然减少了对系统性能的影响,但如果发生崩溃,前一秒尚未写入磁盘的命令操作仍然会丢失。所以,这只能算是避免影响主线程性能和避免数据丢失之间的折衷。

综上,我们可以根据系统对高性能和高可靠性的要求来选择使用哪种回写策略。总之:

  • 如果想要高性能,请选择“no”策略。
  • 如果你想要高可靠性保证,请选择“always”策略。
  • 如果允许一点数据丢失,并且希望性能不会受到太大影响,那么选择“everysec”策略。

不过,根据系统性能需求选择回写策略后,也并非可以“高枕无忧”。毕竟,AOF 以文件的形式记录所有接收到的写入命令。随着接收到的写命令越来越多,AOF文件会变得越来越大。这意味着我们必须小心 AOF 文件过大带来的性能问题。

这里的“性能问题”主要体现在以下三个方面:

  1. 文件系统本身对文件大小由限制,无法保存过大的文件。
  2. 如果文件太大,后续追加命令记录的效率也会降低。
  3. 如果发生崩溃,需要一一重新执行AOF中记录的命令以进行故障恢复。如果日志文件太大,整个恢复过程会非常慢,从而影响Redis的正常使用。

所以,我们需要采取一定的控制措施。这时,AOF重写机制就发挥作用了。

AOF 重写机制

简单来说, AOF重写机制就是Redis根据数据库当前的状态创建一个新的AOF文件。即读取数据库中的所有键值对,然后用一条命令记录其对每个键值对的写入。

例如,读取键值对“ testkey ”:“ testvalue ”后,重写机制会记录命令set testkey testvalue 。这样,当需要恢复时,可以重新执行该命令,实现“ testkey ”:“ testvalue ”的写入。

为什么重写机制可以减小日志文件的大小?事实上,重写机制具有合并功能。即旧日志文件中的多条命令在重写后的新日志文件中变成一条命令。

我们知道,AOF文件以append-only的方式一一记录接收到的写命令。当一个键值对被多个写入命令重复修改时,AOF文件会记录对应的多个命令。

但在重写时,根据该键值对当前的最新状态,为其生成对应的写入命令。这样,一个键值对只需要重写日志中的一条命令。而且日志恢复时,只有执行该命令才能直接完成该键值对的写入。

下图是一个例子:

当我们对一个列表连续进行六次修改操作后,列表的最终状态是[“F”,“E”,“D”]。此时只需使用Lpush mylist DEF命令即可实现此数据的恢复,这样就节省了5个命令的空间。对于已经修改了数百次、数千次的键值对,重写节省的空间当然更大。

不过,虽然AOF重写后日志文件大小会减少,但是将整个数据库的最新数据的操作日志写回磁盘仍然是一个非常耗时的过程。这时候我们还要继续关注另一个问题:重写会阻塞主线程吗?

AOF 重写会阻塞吗?

与AOF日志由主线程记录不同,重写过程由后台子进程bgrewriteaof完成,这也是为了避免阻塞主线程,导致数据库性能下降。

重写过程可以总结为:“一份副本,两份日志”。

“一份副本”是指每次执行重写时,主线程fork出后台bgrewriteaof子进程。此时fork会将主线程的内存复制到bgrewriteaof子进程中,其中包含数据库的最新数据。然后,bgrewriteaof子进程就可以在不影响主线程的情况下,将复制的数据一一写入操作中,并记录在重写日志中。

那么什么是“两份日志”?

由于主线程没有被阻塞,仍然可以处理新的操作。此时如果有写操作,则第一份日志指的是当前使用的AOF日志。 Redis 会将此操作写入其缓冲区。这样,即使系统崩溃,这条AOF日志中的操作仍然是完整的,可以用于恢复。

第二份日志是指新的AOF重写日志。这个操作也会被写入到重写日志的缓冲区中。这样重写日志就不会丢失最新的操作。当所有复制数据的操作记录被重写后,重写日志中记录的这些最新操作也会被写入到新的AOF文件中,以保证记录数据库的最新状态。这时候我们就可以用新的AOF文件替换旧的文件了。

综上所述,每次AOF重写时,Redis都会先进行一次内存复制,以进行重写。然后通过两条日志来保证新写入的数据在重写过程中不会丢失。而且,由于Redis使用额外的线程进行数据重写,因此这个过程不会阻塞主线程。

总结

你可能已经注意到了,磁盘写入时序和重写机制都是在“记录”的过程中发挥作用。例如,写磁盘时机的选择可以避免日志记录时阻塞主线程,重写可以避免日志文件大小过大。但是,在“使用日志”的过程中,即使用AOF进行故障恢复时,我们仍然需要将所有的操作记录重新运行一遍。再加上Redis的单线程设计,这些命令操作只能按顺序一一执行,这个“重放”过程会非常慢。

那么,有没有一种方法既可以避免数据丢失,又可以更快地恢复呢?当然有,那就是RDB快照。我们将在下一篇文章中介绍。

原文链接:,转发请注明来源!