持久化机制-AOF

对于大多数生产环境来说,数据持久化是一个非常重要的课题。Redis 提供了两种持久化方式:RDB(快照)持久化AOF(追加文件)持久化。今天,我们就来重点讨论一下 AOF 持久化,并通过一些具体的例子来分析它的工作原理。

AOF 日志

什么是 AOF ?

AOF(Append Only File)持久化是一种将 Redis 写命令追加到文件中的持久化机制。每次执行写命令时,Redis 会将命令追加到一个日志文件中(即 AOF 文件)。AOF 文件记录的是所有改变数据库状态的命令,因此它能够提供一种比 RDB 更精细的持久化方式。这里需要注意的是,AOF 只记录写操作的命令,但是不会记录读操作的命令。

我们可以把 AOF 比作一份“操作日志”,它记录了所有对数据库的修改操作,从而确保即使 Redis 崩溃,也能够通过这些记录恢复数据。

如何开启 AOF 持久化?

开启 AOF 持久化非常简单,我们只需要在 Redis 配置文件中进行一些设置。默认情况下,AOF 是关闭的。如果我们想开启 AOF,需要打开 Redis 的配置文件,也就是 redis.conf 。然后找到 appendonly 配置项,将它的值改为 yes 就好了。

appendonly yes

AOF 中记录了什么?

假如我们在 Redis 中执行了这样一条命令:

set name xiaobai

AOF 文件是一个普通的文本,我们可以用 cat 命令来查看里面的内容。

如果执行了上面这条命令,那么在 AOF 文件中记录的内容是这样的:

*3
3
SET4
name
$7
xiaobai
  • *3 表示命令有 3 个参数;
  • $3 表示第一个参数的长度是 3( SET );
  • $4 表示第二个参数的长度是 4( name );
  • $7 表示第三个参数的长度是 7( xiaobai )。

这个记录实际上是一个类似于 Redis 命令的序列化形式,它以特定的格式存储,能够完全恢复这条命令。每次 Redis 重启时,AOF 文件会被重新加载,重新执行这些命令,确保数据的恢复。

AOF 的优点

Redis 在执行写命令时,首先会执行该操作,然后再将命令记录到 AOF 文件中,是有一个先后顺序的。这样做有两个好处:

  1. 提高性能:如果 Redis 在执行命令时同时记录日志,会导致性能下降,特别是在高并发的情况下。先执行命令再记录日志,能确保命令操作立即生效,同时记录过程是后台执行的,不影响主线程的性能。
  2. 避免额外的检查开销:如果先把命令记录到 AOF 文件中,再去执行的话,如果这条命令是有语法错误的,那么 AOF 文件中记录的也是错误的命令,后面在进行数据恢复的时候就会出问题了。所以先执行命令,然后再记录,这样就能确保 AOF 文件中记录的命令都是正确的命令。

AOF 的缺点

当然了,AOF 也是有一些缺点的:

  1. 我们已经说了,redis 在执行写命令的时候,是先执行该操作,然后把相应的写命令记录到 AOF 文件。这两个过程有一个先后的顺序。所以,如果 redis 在执行写操作的时候,服务器崩溃,这时命令并没有被记录下来,那么就有能造成数据丢失。
  2. 而且,先执行写操作后记录日志,记录日志这个操作虽然不会影响当前命令的执行,但是有可能会阻塞下一条命令的执行。

三种写回策略

redis 在进行 AOF 持久化的时候,其实有三种策略。

Redis 写入 AOF 日志的过程

在讨论具体的三种写回策略之前,我们首先需要理解 Redis 如何将写操作记录到 AOF 文件中。

  1. 执行写操作命令:当 Redis 收到写操作命令时,它会首先执行这个命令,并更新内存中的数据。
  2. 命令追加到缓冲区:执行命令之后,Redis 会将该命令以文本形式追加到 AOF 缓冲区中。这个缓冲区是在内存中的,所以写入速度非常快。
  3. 系统调用:接下来,Redis 会通过系统调用,将 AOF 缓冲区中的数据传递到操作系统内核。
  4. 内核缓冲区:操作系统内核会将数据写入它自己的缓冲区,内核会根据不同的策略,决定何时将数据真正写入硬盘。
  5. 由内核发起写操作:在适当的时机,内核会将缓冲区中的数据写入硬盘,完成持久化操作。
  6. 硬盘存储:最终,数据被写入磁盘中的 AOF 文件。这样,即使 Redis 重启,AOF 文件中的命令也能恢复数据。

这样就完成了写入 AOF 的基本过程。接下来,我们就重点介绍 Redis AOF 持久化的三种写回策略。

三种写回策略

Redis 提供了三种不同的 AOF 写回策略,分别是 alwayseverysecno。这些策略主要控制 Redis 在将数据从缓冲区写入 AOF 文件时的频率,直接影响 Redis 的性能和数据的安全性。

  • always 策略:每当 Redis 执行一次写命令时,它都会立即将命令写入 AOF 文件。换句话说,每次写操作都会触发一次 fsync() 系统调用,将数据立刻刷到硬盘。
  • everysec 策略:Redis 会将写命令追加到 AOF 缓冲区,但不会立即执行 fsync()。只有每秒钟,Redis 会执行一次 fsync(),将缓冲区中的命令批量写入磁盘。
  • no 策略:在这种策略下,Redis 并不会主动调用 fsync() 来将数据写入硬盘。它只是将数据追加到 AOF 缓冲区,但写操作的持久化由操作系统控制,具体的写入时机取决于操作系统的缓冲策略。

这三种写回策略各有优缺点,但是它们有一个共同的缺点,那就是都无法兼顾 保证数据不丢失和不阻塞主进程

  • always:数据的持久化是最为严格的,几乎没有丢失数据的风险。每个写命令都会被立即持久化。但是由于每次写命令都需要执行 fsync(),因此会极大地影响主进程性能。频繁的磁盘写操作会导致 Redis 的写性能下降,尤其是在高并发情况下。
  • everysec:相比 always 策略,everysec 策略能够显著提高 Redis 的写性能,因为它减少了 fsync() 的频率。每秒执行一次 fsync(),即使在高并发情况下,Redis 也能保持较好的性能。但是数据丢失的风险相对较高,因为如果 Redis 持久化时发生故障(如宕机),最后一秒的写操作可能会丢失。
  • no:性能最好,因为 Redis 不需要管理磁盘写操作。写操作基本不受磁盘 I/O 影响,速度非常快。但是数据丢失的风险最大。如果 Redis 宕机,数据会丢失。操作系统可能会在意外的时刻将数据写入磁盘,因此有可能丢失部分命令。

我们通过对比来更清晰地看到每种策略的优缺点:

策略 数据安全性 性能 适用场景
always 高(每次写操作都持久化) 性能最差 高数据安全性要求的场景(如金融系统)
everysec 中等(每秒持久化一次) 性能较好 对数据安全性要求较高但性能有需求的场景
no 低(操作系统控制持久化) 性能最好 对数据安全性要求不高且高性能优先的场景

fsync() 是一个非常重要的系统调用,它将数据从内存缓冲区刷写到磁盘。如果你足够细心,你就会发现,三种写回策略的不同,就在于调用 fsync() 函数的时机不同:

  • always 策略下,每次 Redis 执行写操作时,都会执行 fsync(),确保数据立即写入硬盘。这是最为严格的持久化方式,几乎没有数据丢失的风险。
  • everysec 策略下,fsync() 只会每秒执行一次。这意味着,尽管写操作很快,但磁盘写操作每秒才发生一次,因此最后一秒的数据可能会丢失。
  • no 策略下,Redis 不会主动调用 fsync(),数据的写入由操作系统控制。这意味着,Redis 本身不会强制将数据写入磁盘,fsync() 的调用取决于操作系统的缓冲区策略。

AOF重写机制

随着时间的推移,AOF 文件会变得越来越大,进而影响 Redis 的性能。因此,Redis 提供了 AOF 重写机制,用来优化 AOF 文件的大小和性能。

为什么需要 AOF 重写机制?

当我们使用 Redis 的并且开启了 AOF 持久化的时候,在不断执行写操作的过程中,AOF 文件会不断增大。比如,每执行一次 SET 命令,该命令会被追加到 AOF 文件中。

想象一下,如果我们向 AOF 文件中添加了成千上万条写命令,这个文件的大小会变得非常庞大,而且文件里包含的每一条命令都非常详细,而且呢,里面还有可能夹杂着很多重复的失效的命令,但是这些命令其实是没有意义的,所以就需要对这些没用的命令进行清理。

如果 AOF 文件很大,不仅会影响 Redis 的启动速度,也会对性能产生负面影响。

AOF重写机制是什么?

AOF 重写机制 会定期创建一个新的 AOF 文件,该文件仅包含能够恢复当前数据状态的最小命令集。

AOF重写机制的工作原理是这样的,它会将现有 AOF 文件中的写操作进行压缩,如果有多个相同的 SET 命令,它会将这些命令压缩成一个命令,减少文件的大小。而且 AOF 重写并不是在每次写操作后都进行的,它是基于一定的条件触发的。在重写过程中,Redis 会创建一个新的 AOF 文件,这个文件会将当前数据库的状态转换为一系列的命令,而不包含冗余的命令。重写会在后台进行,不会阻塞主线程,因此应用可以继续处理写操作。

比如刚开始的时候,我们执行了这两个命令:

SET user:1 "Alice"
SET user:2 "Bob"

后来又执行了很多命令,对数据进行了更新:

SET user:1 "Alice"
SET user:2 "Bob"
SET user:1 "Charlie"  // 更新 user:1
SET user:2 "David"    // 更新 user:2
SET user:3 "Eve"      // 新增 user:3

如果在没有 AOF 重写之前,AOF 文件记录的内容会是这样的,也就是会把所有的命令都记录下来:

SET user:1 "Alice"
SET user:2 "Bob"
SET user:1 "Charlie"
SET user:2 "David"
SET user:3 "Eve"

这个 AOF 文件虽然包含了所有的操作,但可以看到,对于 user:1user:2,有重复的 SET 命令,实际上它们只需要最后一次的修改就能恢复当前的状态。所以,只需要保留以下命令就好了:

SET user:1 "Charlie"
SET user:2 "David"
SET user:3 "Eve"

通过 AOF 重写,Redis 会创建一个新的文件,仅记录最终状态所需的命令,而去除冗余的命令。此时,AOF 文件变得比之前小了,因为它只保留了恢复当前数据状态所需的最少命令。

AOF重写机制的优点

AOF 重写机制的好处有两点。首先,通过压缩命令,去除了一些冗余的写操作,这样就显著减少了 AOF 文件的大小。另外,AOF 文件变小后,Redis 处理这些文件时会更加高效,特别是在启动时,Redis 会更快地加载 AOF 文件。

为什么 AOF 重写机制需要写到一个新的 AOF 文件?

因为 AOF 重写是在后台进行的,Redis 创建一个新的 AOF 文件,不会影响到主线程的操作。这样,Redis 可以继续接受并执行客户端的写命令,保证数据持续写入 AOF 文件。

另外,如果直接修改原始的 AOF 文件,可能会导致文件内容的不一致,进而影响 Redis 的数据恢复。如果 AOF 重写过程中出现了问题,原始文件可能会损坏,而通过创建新文件,可以避免这种风险。

AOF后台重写

为了避免重写过程影响 Redis 的正常工作,Redis 采用了后台重写机制。

为什么是后台重写?

在 Redis 中,所有的读写操作都是由主进程负责的。

如果 AOF 重写操作放在主进程中执行,可能会阻塞主进程。因为 AOF 重写过程需要生成一个新的 AOF 文件并写入磁盘,这个操作可能会占用较长时间。如果把这个过程放在主进程中,Redis 就无法处理其他客户端的请求,造成性能瓶颈,进而影响整个 Redis 实例的响应能力。另外,还会影响正常的写操作。因为如果在主进程中执行 AOF 重写,Redis 在重写过程中就无法继续接受并执行新的写命令。

因此,为了避免上述这两个问题,Redis 选择将 AOF 重写的任务交给后台子进程来完成,从而保证主进程不被阻塞,客户端的请求可以继续正常处理。

Redis 将 AOF 重写操作交给了一个后台子进程,这个子进程由 Redis 内部的命令 bgrewriteaof 启动。这样做有几个好处:

  1. 非阻塞操作:主进程不需要等待 AOF 重写完成,因此可以继续响应客户端请求。Redis 的性能得以保持,保证高并发下的高效处理。
  2. 后台重写无影响:在 AOF 重写过程中,后台子进程会生成一个新的 AOF 文件,同时主进程会继续向原 AOF 文件追加写命令。这样就不会影响正在进行的写操作。

当 Redis 启动 AOF 重写操作时,后台子进程需要创建一个新的 AOF 文件,并将当前 Redis 的数据写入到该文件中。为了确保子进程的数据与主进程一致,Redis 采用了 写时复制(Copy-on-Write,COW)机制。

在 Redis 中,当父进程调用 fork() 创建子进程时,子进程并不会立即复制主进程的数据,而是通过写时复制的方式来实现数据共享。也就是说,主进程和子进程最初共享相同的内存空间,只有在其中一个进程修改数据时,操作系统才会为修改的数据分配新的内存副本,这样就避免了不必要的内存复制。

什么是写时复制

“写时复制”(Copy-on-Write,COW)是一种内存管理技术。简单点说,就是只有在发生写操作的时候,OS 才会去复制物理内存,目的是在进程创建副本时延迟复制数据。具体来说:

  • 在子进程创建时:当父进程调用 fork() 创建子进程时,操作系统并不会立即复制父进程的所有数据,而是让父进程和子进程共享同一块内存区域。
  • 只有在修改时才复制:当父进程或子进程需要修改某个共享的数据时,操作系统会为修改的数据创建一个新的副本,只有修改过的数据才会被复制。这样,大部分数据可以在父子进程之间共享,节省了内存和时间。

子进程可以在不复制所有数据的情况下,操作 AOF 文件并生成新的文件,从而减少了资源消耗。

但是,写时复制也是有缺点的,当父进程调用 fork() 创建子进程时,父进程会被阻塞,直到子进程创建完成。这两个阶段可能会阻塞父进程:

  1. 创建子进程时:当父进程调用 fork() 时,操作系统需要为子进程分配内存,并将父进程的内存页面复制给子进程。在这个过程中,如果页表很大,那么阻塞的时间也就越长。
  2. 创建子进程后:在 fork() 创建子进程后,如果子进程或者父进程修改了数据,就会发生写时复制,这时就会拷贝物理内存,也是会阻塞父进程的。

这两个阶段会导致父进程的阻塞,但由于它们是短暂的,通常不会对 Redis 的性能产生显著影响。

关于 AOF 重写缓冲区

在 Redis 中,AOF 重写缓冲区的作用是暂时存储来自客户端的写命令,直到子进程完成 AOF 重写。这是因为:

  • 避免数据丢失:当 AOF 重写开始时,客户端的写命令会先存入 AOF 重写缓冲区,等待子进程完成重写操作。这样即使子进程正在生成新的 AOF 文件,主进程仍然可以继续接收并处理写请求,保证数据不会丢失。
  • 缓解性能压力:通过使用缓冲区,Redis 可以批量处理写操作,减少磁盘 I/O 的压力,提升系统性能。

bgrewriteaof 子进程执行 AOF 重写期间,主进程仍然需要继续处理客户端请求。具体来说,主进程执行以下工作:

  1. 处理写操作:主进程继续接收客户端的写命令,并将这些命令追加到 AOF 文件中。写命令也会首先存入 AOF 重写缓冲区,等待重写操作完成。
  2. 生成新的 AOF 文件:子进程生成的新 AOF 文件会包含所有当前的数据库状态,而主进程继续将新命令写入缓冲区,待重写完成后将命令写入新的 AOF 文件。
  3. 同步 AOF 文件:当子进程完成 AOF 重写后,主进程会通过一定机制将新生成的 AOF 文件替换掉旧文件,确保数据的持续性和一致性。

总结

这篇文章,我们从头到尾捋了一遍 AOF 的相关知识点,比如 AOF 存在的意义是什么,AOF 的优缺点,在写 AOF 的时候有三种写回策略,三种策略的不同,实际上就是调用 fsync() 函数的时机不同。随着 AOF 文件中命令的增多,所以需要对 AOF 文件进行重写。而且为了避免阻塞主进程,由后台子进程来重写 AOF ,最后还了解了 AOF 缓冲区。

发表评论

后才能评论