Redis——AOF 日志
前言
聊起 Redis 的应用场景,大部分开发者第一反应可能会是:缓存,因为它可以把后端数据库中的数据存储在内存中,内存的访问效率要比硬盘高的多,所以直接从内存中读取数据,响应速度会非常快。
这也是 Redis 目前在业务场景中的普遍使用案例,但其中也暴漏出一个不可忽视的问题:一旦服务器宕机,内存中的数据将会全部丢失。那么该怎么解决这个问题呢?
比较容易想到的解决办法就是,既然 Redis 存储的数据是从后端数据库中来的,那么直接从数据库中恢复不就好了嘛。但这也会引发以下的问题:
- 在恢复的过程中,可能需要频繁地访问数据库,会给数据库带来较大的 IO 压力;
- 这些数据是从数据库中读取出来,性能肯定比不上 Redis 中读取,会影响使用这些数据的应用程序。
由此可见,对于 Redis 来说,实现数据的持久化,避免从后端数据库进行恢复,是至关重要的。目前,Redis 的持久化主要有两种方式,分别是 AOF(Append Only File)日志和 RDB 快照。这篇博客先来看第一种——AOF 日志。
AOF 日志
在传统的后端关系型数据库中对于日志的处理一般都是写前日志(Writer Ahead Log,WAL),简单来说,就是在实际写数据之前,先把修改的数据记录到日志文件中,如果后期数据库出现故障可以方便进行恢复。与此不同的是,AOF 日志刚好相反,它是写后日志,“写后”的意思是 Redis 先执行命令,把数据写入内存,然后才记录日志,如下图所示:
看到这里,你可能会感到疑惑,为什么 AOF 要先执行命令再记录日志呢?回答这个问题之前,先来一起看看 AOF 里面记录了哪些内容。
传统数据库的日志,例如 redo log(重做日志),记录的是修改后的数据,而 AOF 里记录的是 Redis 收到的每一条命令,这些命令是以文本形式保存的。
我们以 Redis 收到“set key1 value1”命令后记录的日志为例,看看 AOF 日志的内容。其中,*3
表示当前命令有三个部分,每部分都是由 $+ 数字
开头,后面紧跟着具体的命令、键或值。这里,“数字”表示这部分中的命令、键或值一共有多少字节。例如,$3 set
表示这部分有 3 个字节,对应的也就是 set
命令。
不过,为了避免额外的检查开销,Redis 在向 AOF 里面记录日志的时候,并不会先去对这些命令进行语法检查。所以,如果先记日志再执行命令的话,如果记录了错误的命令,Redis 在使用日志恢复数据时,就可能会出错。
而写后日志这种方式,就是先让系统执行命令,只有命令被成功执行,才会被记录在日中,否则,系统就会直接向客户端报错。所以,Redis 使用写后日志这一方式的好处就是:可以避免出现记录错误命令的情况。除此之外,AOF 还有一个好处就是:它是在命令执行后才记录日志,所以不会阻塞当前线程的写操作。
但我们知道凡事都有两面性,AOF 也有如下两个潜在的风险。
首先,如果刚执行完一条命令,还没来得及记录日志服务器就宕机了,那么这个命令和相应的数据就会有丢失的风险。如果此时 Redis 是用作缓存,还可以从后端数据库重新读入数据进行恢复,但是,如果 Redis 是直接用作数据库的话,此时,因为命令没有记录日志,所有就无法通过日志进行恢复了。
其次,AOF 虽然避免了对于当前命令的阻塞,但可能会给下一个操作带来阻塞风险。原因也很简单,AOF 日志也是在主线程中执行的,如果在把日志写入磁盘时,IO 压力较大,就会导致写盘很慢,进而导致后续的操作无法正常执行。
诶,到这里如果细心一点,就会发现,刚才所描述的两个风险都是和 AOF 写回磁盘的时间点有关。这也意味着,如果我们能够控制一个写命令执行完 AOF 日志写回磁盘的时机,这两个问题也就迎刃而解咯。
写回策略
其实,对于这个问题,AOF 机制给我们提供了三个选择,也就是 AOF 配置项 appendfsync
的三个可选值:
Always
,同步写回:每个命令执行完成之后,立刻同步地将日志写回磁盘;Everysec
,每秒写回:每个命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;No
,操作系统控制的写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。
针对避免主线程阻塞和减少数据丢失问题,这三种写回策略都无法做到两全其美。我们来分析下其中的原因:
- “同步写回”可以做到基本不丢数据,但是它在每一个写命令后都有一个慢速的落盘操作,不可避免地会影响主线程性能;
- “每秒写回”采用一秒写回一次的频率,避免了“同步写回”的性能开销,虽然减少了对系统性能的影响,但是如果发生宕机,上一秒内未落盘的命令操作仍然会丢失。所以,这也只能算是一种策略上的取舍;
- 虽然“操作系统控制的写回”在写完缓冲区后,就可以继续执行后续的命令,但是落盘的时机已经不在 Redis 把握之中了,只要 AOF 记录没有写回磁盘,一旦宕机对应的数据就丢失了;
配置项 | 写回策略 | 优点 | 缺点 |
---|---|---|---|
Always | 同步写回 | 可靠性高,数据基本不丢失。 | 每个写命令都要落盘,性能影响较大。 |
Everysec | 每秒写回 | 性能适中。 | 宕机时丢失 1 秒内的数据。 |
No | 操作系统控制的写回 | 性能好。 | 宕机时丢失数据较多。 |
到这里,我们就可以根据系统对高性能和高可靠性的要求,来选择使用哪种写回策略了。总结一下就是:想要获得高性能,就选择 No 策略;如果想要得到高可靠性保证,就选择 Always 策略;如果允许数据有一点丢失,又希望性能别受太大影响的话,那么就选择 Everysec 策略。
但是,按照系统的性能需求选定了写回策略,并不是“以逸待劳”了。毕竟,AOF 是以文件的形式在记录接收到的所有写命令。随着接收到的写命令越多,AOF 文件也会越来越大。这也就意味着,一定要小心 AOF 文件过大带来的性能问题。
这里的“性能问题”,主要在于以三个方面:
- 文件系统本身对文件大小有限制,无法保存过大的文件;
- 如果文件太大,之后再往里面追加命令记录的话,效率也会变低;
- 如果服务器发生故障,如果日志文件过大,在恢复过程会非常缓慢,影响 Redis 正常使用。
所以针对以上问题,我们要采取一定的控制手段,那也就是 AOF 重写机制。
AOF 重写机制
AOF 重写机制就是在重写时,Redis 根据数据库的现状创建一个新的 AOF 文件,也就是说,读取数据库中的所有键值对,然后对每一个键值对用一条命令记录它的写入。emmm 这么说的话,可能觉得有点抽象,看个栗子:当读取了键值对 "key1": "value1"
之后,重写机制会记录 set key1 value1
这条命令。这样,当需要恢复时,可以重新执行该命令,实现 "key1": "value1"
的写入。
为什么重写机制可以把日志文件变小呢?实际上,重写机制具有“多变一”的功能。简单来说就是,旧日志文件中的多条命令,在重写后的新日志中变成了一条命令。AOF 文件是以追加的方式,逐一记录接收到的写命令的。当一个键值对被多条写命令反复修改时,AOF 文件会记录相应的多条命令。但是,在重写的时候,是根据这个键值对当前最新的状态,为它生成对应的写入命令。这样一来,一个键值对在重写日志中只用一条命令就行了,而且,在日志恢复时,只用执行这条命令就可以直接完成键值对的写入了。
不过,虽然 AOF 重写后,日志文件会缩小,但是,要把整个数据库的最新数据的操作日志都写会磁盘,也是非常耗时的,这个时候就带来新的问题——“重写会不会阻塞主线程?”
和 AOF 日志由主线程写回不同,重写过程是由后台子进程 bgrewriteaof
来完成的,这也是为了避免阻塞主线程,导致数据库性能下降。
对应过程主要如下:
每次执行重写时,主线程 fork 出后台的 bgrewriteaof
子进程。此时,fork 会把主线程的内存拷贝一份给 bgrewriteaof
子进程,这里面就包含了数据库的最新数据。然后,bgrewriteaof
子进程就可以在不影响主线程的情况下,逐一把拷贝的数据写成操作,记入重写日志。
在这个过程中,由于主线程未阻塞,仍然可以处理新来的操作。此时,如果有写操作,第一处日志就是指正在使用的 AOF 日志,Redis 会把这个操作写到它的缓冲区。这样一来,即使宕机了,这个 AOF 日志的操作仍然是齐全的,可以用于恢复。
还有一点就是新的 AOF 重写日志,这个操作也会被写到日志的缓冲区。这样,重写日志也不会丢失最新的操作。等到拷贝数据的所有操作记录重写完成后,重写日志记录的这些最新操作也会写入新的 AOF 文件,以保证数据库最新状态的记录。这个时候,我们就可以使用新的 AOF 文件替代旧文件了。
总结来说,每次 AOF 重写时,Redis 会先执行一个内存拷贝,用于重写;然后,使用两个日志保证在重写过程中,新写入的数据不会丢失。而且,因为 Redis 采用额外的线程进行数据重写,所以,这个过程并不会阻塞主线程。
总结
在这篇博客中,简单介绍了 Redis 用于避免数据丢失的 AOF 方法。这个方法通过逐一记录操作命令,在恢复时再逐一执行命令的方式,保证了数据的可靠性。
这个方法看似“简单”,但也是充分考虑了对 Redis 性能的影响。总结来说,它提供了 AOF 日志的三种写回策略,分别是 Always、Everysec 和 No,这三种策略在可靠性上是从高到低,而在性能上则是从低到高。
此外,为了避免日志文件过大,Redis 还提供了 AOF 重写机制,直接根据数据库里数据的最新状态,生成这些数据的插入命令,作为新日志。这个过程通过后台线程完成,避免了对主线程的阻塞。
其中,三种写回策略体现了系统设计中的一个重要原则,即 trade-off,或者称为“取舍”,指的就是在性能和可靠性保证之间做取舍。其实,放在生活中也是同理,我们总是在一边得到,一边失去。现如今这个时代,做好取舍,活得自洽就好~~