大家都知道Redis经常被使用在缓存的场景中,那有没有想过这么一个问题,一旦服务器宕机,内存中的数据全部丢失,我们该如何进行恢复呢?如果直接从后端数据库恢复,不仅会给数据库带来巨大的压力,还会使上层应用响应变慢。所以redis的持久化机制是很重要的。接下来我们一起来探讨一下Redis的持久化机制。目前Redis持久化主要有两大机制,即AOF(Append Only File)日志和RDB快照。接下来我们就来分别学习一下。
AOF日志
AOF日志,即写后日志,它的含义是Redis先执行命令,把数据写入内存,然后再写入日志。Redis为什么要先执行命令后写入日志呢?首先我们来看一下AOF日志里记录了什么内容。AOF记录的是Redis收到的每一条命令,这些日志以文本的形式保存。假如我们执行了set hello world命令,AOF的内容如下:
其中“*3”代表当前命令有3部分,每部分是由”$+数字”开头,后面紧跟着具体命令、键和值。这里的数字代表后面的命令、键和值一共有多少个字节。例如 “$3 set”表示这部分有3个字节,也就是“set”命令。
Redis为了避免额外的检查开销,再往AOF里写入日志的时候,并不会对这些命令进行语法检查。所以如果先写日志再执行命令,日志中就可能会记录一些错误的命令,Redis使用日志恢复的时候就会出错。而写后日志这种方式,就是先去执行命令,如果命令执行出错,则不写入日志,只有执行成功的命令才会写入日志中。所以Redis使用写后日志这一方式的一大好处是,可以避免出现记录错误命令的情况。AOF还有一大好处,它是在命令执行后才记录日志,所以不会阻塞当前的写操作。
不过,AOF也有潜在的两个风险,首先如果刚执行完一个命令,还没来得及写日志就宕机了,那么这个命令和相应的数据就有丢失的风险。其次,AOF虽然避免了对当前命令的阻塞,但可能会给下一个操作带来阻塞风险。这是因为,AOF日志也是在主线程中执行的,如何在把日志文件写入磁盘很慢时,就会阻塞后续操作。针对这个问题,AOF给我们提供了三种写回策略,也就是AOF的配置项appendfsync的三个可选值。
- Always,同步写回:每个写命令执行完,立马同步地将日志写回磁盘;
- Everysec,每秒写回:每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;
- No,操作系统控制的写回:每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。
这三种写回策略都无法做到两全其美,都有自己的优缺点,我们只能根据我们的业务场景,是需要高性能还是高可靠性来选择不同的写回策略。
最后,AOF还有一个问题,就是AOF以文件的形式记录在磁盘里。随着Redis接受的写命令越来越多,那么AOF日志文件也会越来越大,所以需要采取一定的手段来控制AOF日志文件的大小。这个时候,AOF重写机制就派上用场了。AOF重写机制是指在重写时,Redis会根据数据库的现状创建一个新的AOF文件,也就是说,读取数据库中的所有键值对,然后对每一个键值对用一条命令记录它的写入。为什么重写机制可以把日志文件变小呢?实际上,重写机制具有“多变一”功能。所谓的“多变一”,也就是说,旧日志文件中的多条命令,在重写后的新日志中变成了一条命令。例如我们对某个key进行了6次写入操作,那么旧的日志文件中就会有6条记录,而重写后的日志文件中就只有一条命令,所以AOF重写会减小文件的大小。那么AOF重写会阻塞主线程吗?毕竟把整个Redis的数据库的最新数据的操作日志都写回磁盘,仍然是一个非常耗时的过程。和AOF日志由主线程写回不同,重写过程是由后台子进程bgrewriteaof来完成的,这也是为了避免阻塞主线程,导致Redis的性能下降。每次执行重写时,主线程 fork 出后台的 bgrewriteaof 子进程。此时,fork 会把主线程的内存拷贝(采用操作系统的写时复制技术,不会真正的拷贝,写时复制技术下面会分析)一份给 bgrewriteaof 子进程,这里面就包含了数据库的最新数据。然后,bgrewriteaof 子进程就可以在不影响主线程的情况下,逐一把拷贝的数据写成操作,记入重写日志。因为主线程未阻塞,仍然可以处理新来的操作。为了避免数据丢失,在AOF重写过程中,新进入的写命令会写入到两份日志中。第一处日志就正在使用的 AOF 日志,Redis 会把这个操作写到它的缓冲区。这样一来,即使宕机了,这个 AOF 日志的操作仍然是齐全的,可以用于恢复。第二处日志,就是指新的 AOF 重写日志。这个操作也会被写到重写日志的缓冲区。这样,重写日志也不会丢失最新的操作。等到拷贝数据的所有操作记录重写完成后,重写日志记录的这些最新操作也会写入新的 AOF 文件,以保证数据库最新状态的记录。此时,我们就可以用新的 AOF 文件替代旧文件了。
RDB快照文件
由于AOF记录的是操作命令,而不是实际的数据。所以,用AOF方法进行故障恢复的时候,需要逐一把操作日志都执行一遍。如果操作日志很多,那Redis恢复的就很慢,影响到正常使用。那有没有既保证可靠性,还能在宕机时实现快速的恢复办法呢?那就是内存快照。对于Redis来说,它把某一时刻的状态以文件的形式写到磁盘上。这样一来,即使宕机,快照文件也不会丢失,数据的可靠性得到了保证。这个快照文件就叫RDB(Redis DataBase)文件。和AOF相比,RDB记录的是某一时刻的数据,并不是操作,所以,在做数据恢复时,直接把RDB文件加载到内存里,很快的完成恢复。
Redis提供了两个命令来生成RDB文件,分别是save和bgsave。
- save:在主线程中执行,会导致阻塞。
- bgsave:创建一个子进程,专门用于写入RDB文件,避免主线程的阻塞。这也是redis生成RDB文件的默认配置。
所以,我们可以采用bgsave来执行全量快照,既保证了可靠性,又避免了Redis的性能影响。接下来,我们来思考这么一个问题,就是Redis在做全量快照时,Redis中的数据可以被修改吗?Redis还支持写操作吗?为了快照而暂停写操作,Redis肯定是不能接受的。所以Redis借助了操作系统提供的写时复制技术(Copy-On-Write,COW)。简单来说,bgsave子进程是由主线程fork生成的,可以共享主线程的所有内存数据。bgsave运行之后,开始读取主线程中的内存数据,并把它们写入RDB文件。此时,如果主线程对这些数据进行读操作,则主线程和bgsave子进程相互不影响。但是,如果主线程执行写操作或者修改操作,也就是修改内存中的一块数据,那么这块数据会复制一份,生成副本。然后主线程在副本上进行修改。同时,bgsave子进程继续把原来的数据写入RDB文件。这样既保证了快照的完整性,也避免了对正常业务的影响。
接下来我们来看下一个问题,就是多久做一次快照,如果快照隔的时间太久,丢的数据就越多,间隔时间太短,丢失的数据越少,但是频繁的执行全量快照会给磁盘带来很大的压力。由于fork创建子进程bgsave这个过程是需要阻塞主线程的,主线程的内存越大,fork时间越长。所以频繁的fork出bgsave子进程,也就会频繁阻塞主线程。那有什么好的办法既能利用RDB的快速恢复,又能以较小的开销做到尽量少丢数据呢。Redis4.0提出了一个混合使用AOF日志和RDB的方法。简单来说,内存快照以一定的频率执行,两次快照之间使用AOF记录操作命令。
最后总结一下,关于AOF和RDB的选择问题,给大家提供3点建议。
- 数据不能丢失时,内存快照和 AOF 的混合使用是一个很好的选择。
- 如果允许分钟级别的数据丢失,可以只使用 RDB。
- 如果只用 AOF,优先使用 everysec 的配置选项,因为它在可靠性和性能之间取了一个平衡。