Redis持久化之RDB与AOF 的区别

Redis 持久化

Redis绝大部分情况都是当做缓存来使用,因为它把后端数据库中的数据存储在内存中,再直接从内存中读取数据,响应速度会非常快

但是有一个不可忽略的问题,一旦服务器宕机,内存中的数据将会全部丢失

我们很容易想到的解决方案是,从后端数据库恢复这些数据,但是这种方式存在两个问题

一个是,需要频繁访问数据库,会给数据库带来巨大的压力

另外,这些数据是从慢速数据库中读取出来的,性能肯定比不上从Redis中读取,导致使用这些数据的应用程序响应也会随之变慢

所以,对于Redis来说,实现数据的持久化,避免从后端数据库中进行恢复是至关重要的

目前,Redis的持久化主要有两大机制

  • AOF日志

  • RDB 快照

我们先来说说 AOF 日志

AOF日志是如何实现的

说到日志,我们比较熟悉的是数据库的写前日志,也就是说,在实际写数据前,先把修改的数据记到日志文件中,以便故障时进行恢复。

不过,AOF日志正好相反,它是写后日志,如下图所示

在这里插入图片描述

Redis首先是先执行命令,把数据写入内存,然后才记录日志,AOF为什么要先执行命令再记日志呢?

要想知道这个问题的答案,我们要先知道AOF里记录了什么内容。

AOF里记录的是Redis收到的每一条命令,这些命令是以文本形式保存的。

在这里插入图片描述

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

其中,“*3”表示当前命令有三个部分,每部分都是由“$+数字”开头,后面紧跟着具体的命令、键或值。

这里,“数字”表示这部分中的命令、键或值一共有多少字节。例如,“$3 set”表示这部分有3个字节,也就是“set”命令。

但是,为了避免额外的检查开销,Redis在向AOF里面记录日志的时候,并不会先去对这些命令进行语法检查。

所以,如果先记日志再执行命令的话,日志中就有可能记录了错误的命令,Redis在使用日志恢复数据时,就可能会出错。

而写后日志这种方式,就是先让系统执行命令,只有命令能执行成功,才会被记录到日志中,否则,系统就会直接向客户端报错。

所以,Redis使用写后日志这一方式的一大好处是,可以避免出现记录错误命令的情况。

除此之外,AOF还有一个好处:它是在命令执行后才记录日志,所以不会阻塞当前的写操作

不过,AOF也有两个潜在的风险。

首先,如果刚执行完一个命令,还没有来得及记录日志就宕机了,那么这个命令和相应的数据就有丢失的风险。

如果此时Redis是用作缓存,还可以从后端数据库重新读入数据进行恢复,但是,如果Redis是直接用作数据库的话,此时,因为命令没有记入日志,所以就无法用日志进行恢复了。

其次,AOF虽然避免了对当前命令的阻塞,但可能会给下一个操作带来阻塞风险。这是因为,AOF日志也是在主线程中执行的,如果在把日志文件写入磁盘时,磁盘写压力大,就会导致写盘很慢,进而导致后续的操作也无法执行了。

仔细分析的话就会发现,这两个风险都是和AOF写回磁盘的时机相关的。

这也就意味着,如果我们能够控制一个写命令执行完后AOF日志写回磁盘的时机,这两个风险就解除了

三种写回策略

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

  • Always,同步写回:每个写命令执行完,立马同步地将日志写回磁盘;
  • Everysec,每秒写回:每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;
  • No,操作系统控制的写回:每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。

针对避免主线程阻塞和减少数据丢失的问题,这三种写回策略都无法做到两全其美。

我们来分析下其中的原因:

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

在这里插入图片描述

到这里,我们就可以根据系统对高性能和高可靠性的要求,来选择使用哪种写回策略了。

总结一下:想要获得高性能,就选择No策略;如果想要得到高可靠性的保证,就选择Always策略;如果允许数据有一点丢失,又希望性能别受太大影响的话,那么就选择Everysec策略。

但是,按照系统的性能需求选定了写回策略,并不是“高枕无忧”了。毕竟,AOF是以文件的形式在记录接收到的所有写命令。随着接收的写命令越来越多,AOF文件会越来越大。这也就意味着,我们一定要小心AOF文件过大带来的性能问题。

这里的“性能问题”,主要在于以下三个方面

  • 文件系统本身对文件大小有限制,无法保存过大的文件

  • 如果文件太大,之后再往里面追加命令记录的话,效率也会变低

  • 如果发生宕机,AOF中记录的命令要一个个被重新执行,用于故障恢复,如果日志文件太大,整个恢复过程就会非常缓慢,这就会影响到Redis的正常使用。

所以,我们就要采取一定的控制手段,这个时候,AOF重写机制就登场了。

日志文件太大了怎么办

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

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

为什么重写机制可以把日志文件变小呢?

实际上,重写机制具有“多变一”功能。所谓的“多变一”,也就是说,旧日志文件中的多条命令,在重写后的新日志中变成了一条命令。我们知道,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重写时,Redis会先执行一个内存拷贝,用于重写;然后,使用两个日志保证在重写过程中,新写入的数据不会丢失。而且,因为Redis采用额外的线程进行数据重写,所以,这个过程并不会阻塞主线程。所以说,AOF这个方法的好处,是每次执行只需要记录操作命令,需要持久化的数据量不大。一般而言,只要采用的不是always的持久化策略,就不会对性能造成太大影响。

但是,也正因为记录的是操作命令,而不是实际的数据,所以,用AOF方法进行故障恢复的时候,需要逐一把操作日志都执行一遍。

如果操作日志非常多,Redis就会恢复得很缓慢,影响到正常使用。这当然不是理想的结果。

那么,还有没有既可以保证可靠性,还能在宕机时实现快速恢复的其他方法呢?

当然有了,这就是接下来要学习的另一种持久化方法:内存快照

RDB 内存快照

所谓内存快照,就是指内存中的数据在某一个时刻的状态记录。这就类似于照片,当你给朋友拍照时,一张照片就能把朋友一瞬间的形象完全记下来。

对于Redis来说,它实现类似照片记录效果的方式,就是把某一时刻的状态以文件的形式写到磁盘上,也就是快照。这样一来,即使宕机,快照文件也不会丢失,数据的可靠性也就得到了保证。

这个快照文件就称为RDB文件,其中,RDB就是Redis DataBase的缩写。和AOF相比,RDB记录的是某一时刻的数据,并不是操作,所以,在做数据恢复时,我们可以直接把RDB文件读入内存,很快地完成恢复。

听起来好像很不错,但内存快照也并不是最优选项。为什么这么说呢?

我们还要考虑两个关键问题:

  • 对哪些数据做快照?这关系到快照的执行效率问题;
  • 做快照时,数据还能被增删改吗?这关系到Redis是否被阻塞,能否同时正常处理请求。

这么说可能你还不太好理解,我还是拿拍照片来举例子。

我们在拍照时,通常要关注两个问题:

  • 如何取景?也就是说,我们打算把哪些人、哪些物拍到照片中;
  • 在按快门前,要记着提醒朋友不要乱动,否则拍出来的照片就模糊了。

你看,这两个问题是不是非常重要呢?

那么,接下来,我们就来具体地聊一聊。

先说“取景”问题,也就是我们对哪些数据做快照。

给哪些数据做快照

Redis的数据都在内存中,为了提供所有数据的可靠性保证,它执行的是全量快照,也就是说,把内存中的所有数据都记录到磁盘中,这就类似于给100个人拍合影,把每一个人都要拍进照片里。这样做的好处是,一次性记录了所有数据,一个都不少。

当你给一个人拍照时,只用协调一个人就够了,但是,拍100人的大合影,却需要协调100个人的位置、状态等等,这当然会更费时费力。同样,给内存的全量数据做快照,把它们全部写入磁盘也会花费很多时间。而且,全量数据越多,RDB文件就越大,往磁盘上写数据的时间开销就越大。

对于Redis而言,它的单线程模型就决定了,我们要尽量避免所有会阻塞主线程的操作。所以,针对任何操作,我们都会提一个灵魂之问:“它会阻塞主线程吗?”

RDB文件的生成是否会阻塞主线程,这就关系到是否会降低Redis的性能。Redis提供了两个命令来生成RDB文件,分别是 save 和 bgsave 。

  • save:是在主线程中执行,会导致阻塞;
  • bgsave:创建一个子进程,专门用于写入RDB文件,避免了主线程的阻塞,这也是Redis RDB文件生成的默认配置。

那么这个时候,我们就可以通过 bgsave 命令来执行全量快照,这既提供了数据的可靠性保证,也避免了对Redis的性能影响。

接下来,我们要关注的问题就是,在对内存数据做快照时,这些数据还能“动”吗?

也就是说,这些数据还能被修改吗? 这个问题非常重要,这是因为,如果数据能被修改,那就意味着Redis还能正常处理写操作。否则,所有写操作都得等到快照完了才能执行,性能一下子就降低了。

快照时数据能修改吗?

在给别人拍照时,一旦对方动了,那么这张照片就拍糊了,我们就需要重拍,所以我们当然希望对方保持不动。对于内存快照而言,我们也不希望数据“动”。

举个例子。我们在时刻 t 给内存做快照,假设内存数据量是4GB,磁盘的写入带宽是0.2GB/s,简单来说,至少需要20s(4/0.2 = 20)才能做完。如果在时刻t+5s时,一个还没有被写入磁盘的内存数据A,被修改成了A1,那么就会破坏快照的完整性,因为A1不是时刻 t 时的状态。因此,和拍照类似,我们在做快照时也不希望数据“动”,也就是不能被修改。但是,如果快照执行期间数据不能被修改,是会有潜在问题的。

对于刚刚的例子来说,在做快照的20s时间里,如果这4GB的数据都不能被修改,Redis就不能处理对这些数据的写操作,那无疑就会给业务服务造成巨大的影响。可能有人会想到可以用 bgsave 避免阻塞啊。

这里我就要说到一个常见的误区了,避免阻塞和正常处理写操作并不是一回事

此时,主线程的确没有阻塞,可以正常接收请求,但是,为了保证快照完整性,它只能处理读操作,因为不能修改正在执行快照的数据。为了快照而暂停写操作,肯定是不能接受的。

所以这个时候,Redis就会借助操作系统提供的写时复制技术(Copy-On-Write, COW),在执行快照的同时,正常处理写操作。简单来说,bgsave子进程是由主线程 fork 生成的,可以共享主线程的所有内存数据。bgsave子进程运行后,开始读取主线程的内存数据,并把它们写入RDB文件。如下图所示

在这里插入图片描述

此时,如果主线程对这些数据也都是读操作,就是这个键值对A,那么,主线程和bgsave子进程相互不影响。但是,如果主线程要修改一块数据(键值对C),那么,这块数据就会被复制一份,生成该数据的副本。然后,bgsave子进程会把这个副本数据写入RDB文件,而在这个过程中,主线程仍然可以直接修改原来的数据。这既保证了快照的完整性,也允许主线程同时对数据进行修改,避免了对正常业务的影响。

到这里,我们就解决了对“哪些数据做快照”以及“做快照时数据能否修改”这两大问题。

Redis会使用 bgsave 对当前内存中的所有数据做快照,这个操作是子进程在后台完成的,这就允许主线程同时可以修改数据。

现在,我们再来看另一个问题,多久做一次快照?我们在拍照的时候,还有项技术叫“连拍”,可以记录人或物连续多个瞬间的状态。

那么,快照也适合“连拍”吗?

可以每秒做一次快照吗?

对于快照来说,所谓“连拍”就是指连续地做快照。这样一来,快照的间隔时间变得很短,即使某一时刻发生宕机了,因为上一时刻快照刚执行,丢失的数据也不会太多。但是,这其中的快照间隔时间就很关键了。如下图所示

在这里插入图片描述

我们先在T0时刻做了一次快照,然后又在 T0+t 时刻做了一次快照,在这期间,数据块5和9被修改了。如果在 t 这段时间内,机器宕机了,那么,只能按照T0时刻的快照进行恢复。此时,数据块5和9的修改值因为没有快照记录,就无法恢复了。

所以,要想尽可能恢复数据,t 值就要尽可能小,t越小,就越像“连拍”。那么,t值可以小到什么程度呢,比如说是不是可以每秒做一次快照?毕竟,每次快照都是由 bgsave 子进程在后台执行,也不会阻塞主线程。这种想法其实是错误的。

虽然bgsave执行时不阻塞主线程,但是,如果频繁地执行全量快照,也会带来两方面的开销

一方面,频繁将全量数据写入磁盘,会给磁盘带来很大压力,多个快照竞争有限的磁盘带宽,前一个快照还没有做完,后一个又开始做了,容易造成恶性循环。

另一方面,bgsave 子进程需要通过fork操作,从主线程创建出来。

虽然,子进程在创建后,不会再阻塞主线程,但是,fork这个创建过程,本身会阻塞主线程,而且主线程的内存越大,阻塞时间越长。如果频繁 fork 出 bgsave 子进程,这就会频繁阻塞主线程了。那么,有什么其他好方法吗?

此时,我们可以做增量快照,所谓增量快照,就是指,做了一次全量快照后,后续的快照只对修改的数据进行快照记录,这样可以避免每次全量快照的开销。也就是说,在第一次做完全量快照后,T1和T2时刻如果再做快照,我们只需要将被修改的数据,写入快照文件就可以了。但是,这么做的前提是,我们需要记住哪些数据被修改了。你可不要小瞧这个“记住”功能,它需要我们使用额外的元数据信息,去记录哪些数据被修改了,这会带来额外的空间开销问题,如图所示

在这里插入图片描述

如果我们对每一个键值对的修改,都做个记录,那么,如果有1万个被修改的键值对,我们就需要有1万条额外的记录。而且,有的时候键值对非常小,比如只有32字节,而记录它被修改的元数据信息,可能就需要8字节。

这样的话,为了“记住”修改,引入的额外空间开销就会比较大。这对于内存资源宝贵的Redis来说,有些得不偿失。

到这里,你可以发现,虽然跟AOF相比,快照的恢复速度快,但是,快照的频率不好把握,如果频率太低,两次快照间一旦宕机,就可能有比较多的数据丢失。

如果频率太高,又会产生额外开销,那么,还有什么方法既能利用RDB的快速恢复,又能以较小的开销做到尽量少丢数据呢?

Redis 4.0中提出了一个混合使用AOF日志和内存快照的方法。简单来说,内存快照以一定的频率执行,在两次快照之间,使用AOF日志记录这期间的所有命令操作。这样一来,快照不用很频繁地执行,这就避免了频繁 fork 对主线程的影响。而且,AOF日志也只用记录两次快照间的操作,也就是说,不需要记录所有操作了,因此,就不会出现文件过大的情况了,也可以避免重写开销。如图所示

在这里插入图片描述

T1和T2时刻的修改,用AOF日志记录,等到第二次做全量快照时,就可以清空AOF日志,因为此时的修改都已经记录到快照中了,恢复时就不再用日志了。这个方法既能享受到RDB文件快速恢复的好处,又能享受到AOF只记录操作命令的简单优势

持久化的具体操作

光说理论没有用,接下来具体操作。先来看看快照的。

打开 redis.conf 配置文件,我们要找 SNAPSHOTTING ,这个单词就是快照的意思

它的格式就是这样的 save <senconds> <changes>

第一参数表示的是 指定时间间隔,第二个参数表示的是 执行指定次数的更新操作。满足这两个两个条件,会把内存中的数据同步到硬盘中。

Redis中 默认 1个小时,也就是 3600 秒内有1个更改;或者 5分钟,300百秒内,有100个更改;又或者 60 秒内 1万次的更改。

也就是说 默认满足这三个条件中的其中一个,就会将内存中的数据快照写入磁盘中,如果不想用Redis 默认的,可以把注释打开,自行修改。还有就是指定本地数据库的文件名是什么,叫这个名字(dbfilename ),一般采用默认的。我们重新配置 save ,配置成 150 秒 3次

在这里插入图片描述

可以看到 3次 操作生成了 快照文件

在这里插入图片描述

接下来通过 RDB 恢复数据

  • 首先备份 dump.rdb为 dump_bak.rdb(模拟线上环境)
  • 接下来使用一个命令 flushall 清空数据(模拟数据丢失,这个地方要注意一下:flushall 也会触发 rdb 持久化)
  • 然后 将 dump_bak.rdb 替换 dump.rdb
  • 重启 redis 服务,恢复数据
[root@localhost bin]# cp dump.rdb dump_bak.rdb 
[root@localhost bin]# ./redis-cli
127.0.0.1:6379> flushall
48991:M 01 Nov 2021 01:01:55.827 * DB saved on disk
OK
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> shutdown
not connected> exit
[root@localhost bin]# cp dump_bak.rdb dump.rdb
cp: overwrite ‘dump.rdb’? y
[root@localhost bin]# ./redis-server redis.conf &
[root@localhost bin]# ./redis-cli 
127.0.0.1:6379> keys *
1) "testkey"
2) "hello"
127.0.0.1:6379> exit

这就是 RDB快照 的操作

接下来再来看下 AOF 是如何操作的

首先是在配置文件中,找到有关于 appendonly 有关的内容,AOF 默认是关闭的,我们需要把它打开,把 no 改为 yes,文件名就用默认的不改了,指定更新的日志的条件, 叫这个名字 appendfsync ,这三个配置刚才已经讲过了,我们就用 always

还有一个是 配置重写触发机制是下面这两个配置

auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

这两个配置的意思是,当AOF 文件的大小是上次 rewrite 后大小的一倍,且文件大于 64M时触发。一般都是设置为 3G,因为64M太小了

接下来具体来操作,如何恢复数据的

  • 首先执行 flushall,模拟数据丢失
  • 重启 redis 服务,恢复数据
  • 紧接着 修改 appendonly.aof ,模拟文件异常
  • 然后再重启 redis ,服务启动不起来,失败了。同时也说明,RDB 和 AOF 可以同时存在,且优先加载 AOF 文件
  • 这时候使用 redis-check-aof 校验 appendonly.aof 文件
  • 再重启 Redis ,服务正常

根据这个流程来操作一下

创建模拟数据

[root@localhost bin]# ./redis-cli
127.0.0.1:6379> set hello codeman
OK
127.0.0.1:6379> exit

127.0.0.1:6379> shutdown
not connected> exit
[root@localhost bin]# cp dump_bak.rdb dump.rdb
cp: overwrite ‘dump.rdb’? y
[root@localhost bin]# ./redis-server redis.conf &
[root@localhost bin]# ./redis-cli 
127.0.0.1:6379> keys *
1) "testkey"
2) "hello"
127.0.0.1:6379> exit

模拟数据丢失

[root@localhost bin]# ./redis-cli
127.0.0.1:6379> flushall
127.0.0.1:6379> keys *
127.0.0.1:6379> shutdown
not connected> exit
[root@localhost bin]# ./redis-server redis.conf &
[root@localhost bin]# ./redis-cli 
127.0.0.1:6379> keys *
1) "testkey"
2) "hello"
127.0.0.1:6379> exit

这时候发现数据没有恢复是个什么情况?

这里有一点要注意,当你使用 flushall 清空数据的时候,重启 redis 服务,数据没有恢复的话,是因为 flushall 命令,也被写入到了 AOF 文件中,导致数据恢复失败,这时候只需要删除 aof 文件中的 flushall 就行了。

接下来我们再来模拟一下 AOF 文件异常,随便在 AOF 文件中输入一些数据,保存

启动 Redis 服务,再连接 Redis

[root@localhost bin]# ./redis-server redis.conf &
[root@localhost bin]# ./redis-cli 

发现连接不上,这时候要去修复 AOF 文件,通过下面这个命令修复 AOF 文件

[root@localhost bin]# redis-check-aof --fix appendonly.aof

恢复了之后,再重新启动Redis 服务,连接Redis ,查看数据是否恢复。

我们接下来再来看下 混合模式

当我们开启了混合持久化时,启动redis依然优先加载aof文件。

aof文件加载可能有两种情况:

  • aof文件开头是rdb的格式, 先加载 rdb内容再加载剩余的 aof。

  • aof文件开头不是rdb的格式,直接以aof格式加载整个文件。

我们通过命令操作一下,通过bgrwriteaof完成

127.0.0.1:6379> set key1 value1
OK
127.0.0.1:6379> set key2 value2
OK
127.0.0.1:6379> bgrewriteaof
127.0.0.1:6379> set key3 value3
OK
127.0.0.1:6379> exit

此时的aof文件已经和只开启AOF持久化文件不一样了,一部分数据来自RDB文件,一部分来自Redis运行过程时的增量数据

在这里插入图片描述
这就是 混合模式

了解更多可扫码关注公众号
在这里插入图片描述

posted on 2021-12-13 22:20  凝神遐想  阅读(797)  评论(0编辑  收藏  举报

导航