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运行过程时的增量数据
这就是 混合模式
了解更多可扫码关注公众号