当被问到在哪些业务场景下你会使用Redis时,你很可能会回答:“我会将其用作缓存,因为Redis将后端数据库中的数据存储在内存中,然后直接从内存中读取数据,因此响应速度非常快。”没错,这确实是Redis的一种常见使用场景,但也存在一个绝对不能忽视的问题:一旦服务器宕机,内存中的数据将全部丢失。
解决这个问题的一个显而易见的方法是从后端数据库中恢复这些数据。然而,这种方法存在两个问题:首先,频繁访问数据库会给数据库带来巨大的压力;其次,这些数据是从较慢的数据库中读取出来的,性能肯定不如从Redis中读取,这会导致使用这些数据的应用程序响应速度变慢。因此,对于Redis来说,实现数据持久化以避免从后端数据库进行恢复是至关重要的。
目前,Redis实现数据持久化主要依靠两种机制,即AOF(Append-Only File)日志和RDB快照。在接下来的两个部分,我们将分别深入探讨这两种机制。首先,让我们重点关注AOF日志。
AOF 日志是如何实现的?
说到日志,我们比较熟悉的是数据库的写前日志(Write Ahead Log, WAL),也就是说,在实际写数据前,先把修改的数据记到日志文件中,以便故障时进行恢复。不过,AOF 日志正好相反,它是写后日志,“写后”的意思是 Redis 是先执行命令,把数据写入内存,然后才记录日志,如下图所示:
Redis AOF操作过程
那 AOF 为什么要先执行命令再记日志呢?要回答这个问题,我们要先知道 AOF 里记录了什么内容。
传统数据库的日志,例如 redo log(重做日志),记录的是修改后的数据,而 AOF 里记录的是 Redis 收到的每一条命令,这些命令是以文本形式保存的。
我们以 Redis 收到“set testkey testvalue”命令后记录的日志为例,看看 AOF 日志的内容。其中,“*3”表示当前命令有三个部分,每部分都是由“$+数字”开头,后面紧跟着具体的命令、键或值。这里,“数字”表示这部分中的命令、键或值一共有多少字节。例如,“$3 set”表示这部分有 3 个字节,也就是“set”命令。
Redis AOF日志内容
但是,为了避免额外的检查开销,Redis 在向 AOF 里面记录日志的时候,并不会先去对这些命令进行语法检查。所以,如果先记日志再执行命令的话,日志中就有可能记录了错误的命令,Redis 在使用日志恢复数据时,就可能会出错。
而写后日志这种方式,就是先让系统执行命令,只有命令能执行成功,才会被记录到日志中,否则,系统就会直接向客户端报错。所以,Redis 使用写后日志这一方式的一大好处是,可以避免出现记录错误命令的情况。
除此之外,AOF 还有一个好处:它是在命令执行后才记录日志,所以不会阻塞当前的写操作。
不过,AOF 也有两个潜在的风险。
首先,考虑一种情况:如果刚刚执行完一个命令,还来不及将该命令记录到日志中,Redis服务器突然宕机,那么这个命令以及相关数据可能会丢失。当Redis被用作缓存时,数据可以从后端数据库重新加载以进行恢复。但是,如果Redis被直接用作数据库,由于命令尚未被记录到日志中,因此无法使用日志进行数据恢复。
其次,尽管AOF机制避免了当前命令的写入日志时的阻塞,但这可能会带来潜在的阻塞风险。这是因为AOF日志写入也是在Redis的主线程中进行的。如果在将日志写入磁盘时,磁盘写入压力很大,这将导致写入操作非常缓慢,从而影响后续操作的执行。
经过仔细分析,你会发现这两种风险都与AOF写回磁盘的时机有关。这也就表明,如果我们能够精确控制命令执行后AOF日志写回磁盘的时机,那么这两种风险就能够得到有效地解决。
三种写回策略
关于AOF机制的问题,有三种不同的策略,对应于AOF配置项appendfsync的三个可选值。
- Always,同步写回:在每个写命令执行完后,它会立即将日志同步写回到磁盘。
- Everysec,每秒写回:在每个写命令执行完后,它仅将日志写入AOF文件的内存缓冲区,然后每隔一秒才将缓冲区中的内容写入磁盘。
- No,操作系统控制的写回:在每个写命令执行完后,它也只是将日志写入AOF文件的内存缓冲区,而将写回磁盘的时机由操作系统控制。
然而,针对避免主线程阻塞和减少数据丢失问题,这三种写回策略都无法做到完美的平衡。以下是对它们的分析:
- 同步写回可以基本保证数据不会丢失,但它在每个写命令之后都需要执行一个相对较慢的落盘操作,这不可避免地会影响主线程的性能。
- 尽管操作系统控制的写回在写完缓冲区后可以继续执行后续命令,但它失去了对落盘时机的控制,只要AOF记录未写回磁盘,一旦发生宕机,对应的数据就会丢失。
- 每秒写回采用了一秒写回一次的频率,避免了与“同步写回”相关的性能开销,但如果发生宕机,上一秒内未写回磁盘的命令操作仍然会丢失。因此,这个策略可以视为在避免影响主线程性能和避免数据丢失之间的一种妥协。
我把这三种策略的写回时机,以及优缺点汇总在了一张表格里,以方便你随时查看。
在这个阶段,我们可以根据系统对高性能和高可靠性的需求来选择适合的写回策略。总结来说:
- 如果追求高性能,可以选择No策略。
- 如果需要高可靠性保证,应选择Always策略。
- 如果能够容忍一定程度的数据丢失,同时希望性能受到较小影响,那么Everysec策略是一个不错的选择。
然而,仅仅根据系统性能需求选择写回策略并不能完全保障系统的顺利运行。这是因为AOF以文件形式记录接收到的所有写命令。随着写命令的不断增加,AOF文件会变得越来越大。这会引发性能问题,主要体现在以下三个方面:
- 文件系统本身对文件大小有限制,无法容纳过大的文件。
- 如果AOF文件过大,继续往其中追加命令记录将导致效率下降。
- 在发生宕机时,AOF文件中记录的每个命令都必须逐个重新执行以进行故障恢复。如果AOF文件过大,恢复过程将变得极其缓慢,从而影响Redis的正常运行。
因此,我们必须采取控制措施来解决AOF文件过大的问题,而AOF重写机制就是其中一种解决方案。
日志文件太大了怎么办?
AOF重写机制的核心原理很简单,它的任务是根据数据库的当前状态,创建一个全新的AOF文件。这意味着它需要读取数据库中的所有键值对,然后为每个键值对生成一条相应的写入命令。例如,当它读取键值对"testkey": "testvalue"时,重写机制会记录一条"set testkey testvalue"的命令。这样,当需要进行故障恢复时,可以重新执行这些命令,以还原"testkey": "testvalue"的写入操作。
那么,AOF重写机制如何帮助减小日志文件的大小呢?实际上,它具有“多对一”的功能。这意味着,旧日志文件中的多个命令在重写后的新日志中变成了一条命令。
我们知道,AOF文件以追加的方式记录接收到的写命令。当一个键值对经历多次修改时,AOF文件会记录多条相应的写命令。然而,在重写过程中,根据键值对的当前状态,只生成一条对应的写入命令。这意味着,一个键值对在重写后的日志中只需要一条命令,而在日志恢复时,只需执行这一条命令,即可完全还原该键值对的写入操作。这种方式显著减小了AOF文件的体积,提高了性能。
下面这张图就是一个例子:
AOF重写减少日志大小
当我们对一个列表先后做了 6 次修改操作后,列表的最后状态是[“D”, “C”, “N”],此时,只用 LPUSH u:list “N”, “C”, "D"这一条命令就能实现该数据的恢复,这就节省了五条命令的空间。对于被修改过成百上千次的键值对来说,重写能节省的空间当然就更大了。
不过,虽然 AOF 重写后,日志文件会缩小,但是,要把整个数据库的最新数据的操作日志都写回磁盘,仍然是一个非常耗时的过程。这时,我们就要继续关注另一个问题了:重写会不会阻塞主线程?
AOF 重写会阻塞吗?
和 AOF 日志由主线程写回不同,重写过程是由后台线程 bgrewriteaof 来完成的,这也是为了避免阻塞主线程,导致数据库性能下降。
我把重写的过程总结为“一个拷贝,两处日志”。
“一个拷贝”就是指,每次执行重写时,主线程 fork 出后台的 bgrewriteaof 子进程。此时,fork 会把主线程的内存拷贝一份给 bgrewriteaof 子进程,这里面就包含了数据库的最新数据。然后,bgrewriteaof 子进程就可以在不影响主线程的情况下,逐一把拷贝的数据写成操作,记入重写日志。
“两处日志”又是什么呢?
因为主线程未阻塞,仍然可以处理新来的操作。此时,如果有写操作,第一处日志就是指正在使用的 AOF 日志,Redis 会把这个操作写到它的缓冲区。这样一来,即使宕机了,这个 AOF 日志的操作仍然是齐全的,可以用于恢复。
而第二处日志,就是指新的 AOF 重写日志。这个操作也会被写到重写日志的缓冲区。这样,重写日志也不会丢失最新的操作。等到拷贝数据的所有操作记录重写完成后,重写日志记录的这些最新操作也会写入新的 AOF 文件,以保证数据库最新状态的记录。此时,我们就可以用新的 AOF 文件替代旧文件了。
AOF非阻塞的重写过程
总结来说,每次 AOF 重写时,Redis 会先执行一个内存拷贝,用于重写;然后,使用两个日志保证在重写过程中,新写入的数据不会丢失。而且,因为 Redis 采用额外的线程进行数据重写,所以,这个过程并不会阻塞主线程。
小结
这篇文章详细介绍了Redis中用于防止数据丢失的AOF(Append-Only File)方法。这种方法通过逐一记录执行操作命令的方式,以确保在需要数据恢复时能够逐一执行这些命令,从而保证了数据的可靠性。
虽然这一方法看似相对简单,但它充分考虑了对Redis性能的潜在影响。总结来看,AOF日志提供了三种不同的写回策略,分别为Always、Everysec和No。这三种策略在可靠性方面从高到低排序,但在性能方面则正好相反,从低到高。
此外,为了避免AOF日志文件变得过大,Redis还引入了AOF重写机制。该机制通过后台线程根据数据库内数据的最新状态生成相应的插入命令,作为新的AOF日志。这个过程的设计避免了对主线程的阻塞,从而提高了系统的整体性能。
这三种写回策略突显了系统设计中的重要原则,即权衡。这意味着需要在性能和可靠性之间找到平衡。我相信这一原则对于系统设计和开发来说至关重要,希望你能深刻理解它,并在实际开发中应用得当。
然而,需要注意的是,数据持久化和AOF重写机制主要在"记录日志"过程中发挥作用。例如,选择合适的数据持久化时机可以避免在记录日志时阻塞主线程,而AOF重写机制则可以防止AOF日志文件无限增长。但在"使用日志"的过程中,即使用AOF进行故障恢复时,所有操作记录仍然需要逐一执行。考虑到Redis的单线程设计,这个"重放"过程可能会比较慢。
然而,是否有一种方法既能确保数据不丢失,又能更快地进行数据恢复呢?当然,这就是RDB快照的用武之地。在下一篇文章中,我们将深入研究RDB快照的工作原理和应用。