Redis是怎么进行数据持久化的?RDB和AOF
1.Redis为什么要做持久化
我们知道,Redis是一个基于内存的缓存数据库,但是基于内存就有个问题,服务器一down机,内存中的数据就会丢失。这时候,不论我们把Redis作为缓存还是数据库使用,是不可接受的。所以我们需要把数据落到磁盘上,做数据的持久化。那redis持久化的方式有哪些呢?且看我们往下分解。
2.RDB
RDB,英文全称是Redis DataBase,是一种基于内存快照的持久化方式。内存快照是指内存数据在某一时刻的状态记录。正是由于RDB记录的是某一时刻的数据,所以在进行数据恢复的时候,我们可以直接把数据读到内存,很快完成恢复。
1.全量数据持久化
Redis使用RDB的方式进行数据持久化,是进行全量数据的处理,即全量快照。它有两种命令方式:save和bgsave。这两种方式如下:
- save:执行该命令后,redis会在主进程上进行,会产生阻塞;
- bgsave:执行该命令后,redis会fork出一个子进程,专门用于写入redis的RDB文件,避免了主进程的阻塞。
这两做一个小的说明,那就是redis默认是执行哪种命令来执行redis的持久化呢?我之前也是有疑惑的,在这里我说一下我的疑惑产生的原因:因为redis的配置文件中,有save的配置参数:
save 900 1
save 300 10
save 60 10000
所以我就以为redis内存默认会自动采用save的方式进行持久化,其实这是一个理解上的错误。上面配置文件中的save配置,其实是用来说明进行RDB快照的时机,而不是说save命令。
2.内存快照进行时,还能进行读写吗
我们知道,使用bgsave的时候,主进程会fork出一个子进程用于RDB数据的写入,这时候主进程是没有阻塞的。那我们可能会说,那redis肯定能够进行读写啊,因为redis的主进程没有阻塞啊。其实这是一个误区,阻塞和读写是两个概念,两者没有什么大的相关性。不阻塞不一定就能够读写,因为我们读写,最终是要得到或者更新数据,与阻塞和非阻塞没有什么太大关系。
bgsave子进程,只是从主进程fork出的一个进程,此时bgsave子进程会共享主进程的所有内存数据。但这样在进行数据写操作时可能就会比较麻烦,因为两个进程共享一个内存数据,那么就会产生并发,内存数据就会有竞争,处理不好就会产生数据的不一致。那怎么结局这个问题呢?其实Redis借助了操作系统的写时复制(Copy On Write)机制,在bgsave子进程执行快照的时候,主进程正常进行读写。在bgsave子进程执行过程中,此时如果主进程进行读,由于二者是单独的进程,所以读没有问题;如果主进程写的时候,不是新增操作,而是一个修改操作,那么这时候就会对该修改的数据生成一小块内存副本,子进程把内存副本写入的RDB,而主进程仍然可以正常的操作原来的那一小块数据内存。
3.内存快照执行的频率
我们先考虑一下,利用bgsave子进程来进行快照处理,一旦redis发生down机事故,恢复时是否能够完全进行数据恢复而没有丢失么?
们先在 t0 时刻做了一次快照,然后又在 t2时刻做了一次快照,在这期间的t1时刻,数据块 5 和 6 被修改了。如果在 t1和t2 这段时间内,机器宕机了,那么,只能按照 t0 时刻的快照进行恢复。此时,数据块 5 和 96的修改值因为没有快照记录,就无法恢复了。所以,我们如果使得t2和t0之间的时间间隔足够足够小,就能够在很大程度上减少数据的丢失(只能减少,不可能完全避免)。那么设置成足够小会有问题么?由于bgsave是一个单独的子进程,貌似是没有问题的,但其实问题没有这么简单,为什么的?虽然 bgsave 执行时不阻塞主进程,但是,如果频繁地执行全量快照,也会带来两方面的开销。
- 频繁将全量数据写入磁盘,会给磁盘带来很大压力,多个快照竞争有限的磁盘带宽,前一个快照还没有做完,后一个又开始做了,容易造成恶性循环。
- bgsave 子进程需要通过 fork 操作从主进程创建出来。虽然,子进程在创建后不会再阻塞主进程,但是,fork 这个创建过程本身会阻塞主进程,而且主进程的内存越大,阻塞时间越长。如果频繁 fork 出 bgsave 子进程,这就会频繁阻塞主进程了。
4.增量快照
从上面的知识我们知道bgsave子进程无法实现数据的完全不丢失,所以根据以前的知识,我们可以做数据增量,做增量快照,这也是很多数据库中采取的一种方式。增量快照,就是指,做了一次全量快照后,后续的快照只对修改的数据进行快照记录,这样可以避免每次全量快照的开销。
假如在t0时刻做了一次全量快照,t1、t2、t3时刻对数据进行了修改,此时就要开辟一个内存空间做一个修改记录的存储,假设有瞬间有10万个数据修改,那么可想而知,此时的内存空间会是不小的开销,得不偿失。
Redis 4.0 中提出了一个混合使用 AOF 日志和内存快照的方法。简单来说,内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作。
这样一来,快照不用很频繁地执行,这就避免了频繁 fork 对主进程的影响。而且,AOF 日志也只用记录两次快照间的操作,也就是说,不需要记录所有操作了,因此,就不会出现文件过大的情况了,也可以避免重写开销。
这个方法既能享受到 RDB 文件快速恢复的好处,又能享受到 AOF 只记录操作命令的简单优势,
关于AOF,我们下面会详细介绍。
3.AOF
在第二部分我们主要介绍了RDB,基于内存全量快照的数据持久化,这一部分我们介绍第二种持久化的方式:AOF。
1.AOF是什么
AOF的英文全称是Append Only File,是redis使用日志的方式来持久化数据的一种方式。AOF采用“写后日志”的方式,记录redis的操作命令(注意:aof记录的并不是内存中的全量数据,而是redis一条一条的执行命令)。相比我们之前熟悉的一些数据库,特别是mysql这种关系型数据库,都是采用“写前日志”的方式,那么redis为什么采用“写后日志”呢?
通过上面的叙述可以知道,AOF日志中记录的是一条一条的的执行命令,而AOF记录这些命令的时候,会先检测命令的正确性(比如语法、语义)等,所以,如果先记日志再执行命令的话,日志中就有可能记录了错误的命令,Redis 在使用日志恢复数据时,就可能会出错。而采用写后日志的方式,就会让redis主进程先执行命令,执行成功后才写入AOF日志,否则就会向客户端返回错误,所以Redis 使用写后日志这一方式的一大好处是,可以避免出现记录错误命令的情况。除此之外,采用“写后日志”,可以避免阻塞当前写操作。
2.AOF日志落盘的三大策略
AOF日志可能产生的两个问题:
- 如果redis刚执行完一个写操作的命令,这时候还没有来得及记录AOF的操作日志,redis所在服务器就down掉了,那么这个命令就有极大的丢失风险;
- AOF虽然可以使当前的写操作命令非阻塞,但是AOF写入日志是在主进程中进行的,如果AOF日志写入的时间过长,就会导致下一个操作等待时间过长或者超时无法操作;
而要解决这两个问题,其实就是要解决AOF日志写回的时机。
对于这个问题,AOF 机制给我们提供了三个选择,也就是 AOF 配置项 appendfsync 的三个可选值,如下配置截图:
Always,同步写回:每个写命令执行完,立马同步地将日志写回磁盘;
**Everysec,每秒写回:**每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;
**No,操作系统控制的写回:**每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。
但很可惜,这三种策略都有各自的问题,并不完美。下面是三种策略的比较:
配置参数 | 解释 | 优点 | 缺点 |
---|---|---|---|
Always | 同步写回 | 数据可靠性高,基本不会丢失 | 每个写命令都要落到磁盘,极大影响性能 |
Everysec | 每秒写回 | 基本可以保证大部分数据不会丢失,性能也不会影响过大 | 宕机时会丢失1秒的数据 |
No | 操作系统控制的写回 | 性能比较好 | 宕机时丢失的数据会比较多 |
我们可以根据自己项目对高性能和高可靠性的要求,来选择使用哪种写回策略了想要获得高性能,就选择 No 策略;如果想要得到高可靠性保证,就选择 Always 策略;如果允许数据有一点丢失,又希望性能别受太大影响的话,那么就选择 Everysec 策略。
3.AOF的重写机制
什么是AOF的重写机制?为什么要进行AOF的重写。其实这还是要从性能角度来考虑。因为AOF是采用写日志的方式,将命令追加到文件中,随着我们的使用时间越长,执行的命令越多,自然就会导致我们的AOF日志的大小逐渐变大。众所周知,一个文件变大,那么在对文件进行先关操作时,就会产生文件操作的性能问题,主要表现的以下三个方面:
- 如果文件太大,之后再往里面追加命令记录的话,效率会变低;
- 文件系统本身对文件大小有限制,无法保存过大的文件;
- 如果发生宕机,AOF 中记录的命令要一个个被重新执行,用于故障恢复,如果日志文件太大,整个恢复过程就会非常缓慢,这就会影响到 Redis 的正常使用。
基于上面这三个问题,所以我们要对AOF日志进行控制,这就产生了AOF日志的重写机制。
AOF 重写机制就是在重写时,Redis 根据数据库的现状创建一个新的 AOF 文件,即读取数据库中的所有键值对,然后对每一个键值对用一条命令记录它的写入。AOF重写机制之所以会是文件变小,其实就是利用了“多变一”,即对同一个键值对的多个操作命令,合并为一条操作命令。
4.AOF的重写的过程
AOF的重写过程是由主进程fork出一个bgrewriteaof子进程,单独用于AOF日志的重写,这样做的好处是不阻塞主进程,这个RDB中的bgsave有异曲同工之处。
每次 AOF 重写时,Redis 会先执行一个内存拷贝(其实开始是内存共享,然后根据主进程的写命令,做Copy-On-Write),用于重写;然后,使用两个日志保证在重写过程中,新写入的数据不会丢失。而且,因为 Redis 采用额外的进程进行数据重写,所以,这个过程并不会阻塞主进程。
5.AOF自动重写条件
上面我们介绍了AOF重写的过程,那除了我们手动执行bgrewriteaof命令进行AOF日志的重写,redis本身是在什么条件下触发自动重写呢?
如果要用redis的自动触发,首先也是最重要的,AOF的功能要开启,配置为:appendonlyfile yes。
在AOF功能开启的情况下,AOF自动重写条件主要涉及到下面三个变量:
- 记录当前AOF文件大小的变量aof_current_size
- 记录最后一次AOF重写之后,AOF文件大小的变量aof_rewrite_base_size
- 增长百分比变量aof_rewrite_perc
每当serverCron函数(redis的crontab)执行时,会检查以下条件是否全部满足,如果是的话,就会触发自动的AOF重写: - 没有 BGSAVE 命令在执行
- 没有 BGREWRITEAOF 在执行
- 当前AOF文件大小 > server.aof_rewrite_min_size(默认为1MB)
- 当前AOF文件大小和最后一次AOF重写后的大小之间的比率大于等于指定的增长百分比(默认为1倍,100%)
默认情况下,增长百分比为100%。也就是说,如果前面三个条件已经满足,并且当前AOF文件大小比最后一次AOF重写的大小大一倍就会触发自动AOF重写。
6.AOF的重写机制的思考
1.AOF重写日志会产生阻塞风险么?
Redis采用fork子进程重写AOF文件时,潜在的阻塞风险包括:fork子进程 和 AOF重写过程中父进程产生写入的场景。
- fork子进程:主进程fork子进程,fork这个瞬间一定是会阻塞主进程的阻塞,fork采用操作系统提供的写实复制(Copy On Write)机制,就是为了避免一次性拷贝大量内存数据给子进程造成的长时间阻塞问题,但fork子进程需要拷贝进程必要的数据结构,其中有一项就是拷贝内存页表(虚拟内存和物理内存的映射索引表),这个拷贝过程会消耗大量CPU资源,拷贝完成之前整个进程是会阻塞的,阻塞时间取决于整个实例的内存大小,实例越大,内存页表越大,fork阻塞时间越久。拷贝内存页表完成后,子进程与父进程指向相同的内存地址空间,也就是说此时虽然产生了子进程,但是并没有申请与父进程相同的内存大小,只有在写发生时,才真正拷贝内存真正的数据,这个过程中,父进程也可能会产生阻塞的风险。
- AOF重写过程中父进程产生写入: fork出的子进程指向与父进程相同的内存地址空间,此时子进程就可以执行AOF重写,把内存中的所有数据写入到AOF文件中。但是此时父进程依旧是会有流量写入的,如果父进程操作的是一个已经存在的key,那么这个时候父进程就会真正拷贝这个key对应的内存数据,申请新的内存空间,这样逐渐地,父子进程内存数据开始分离,父子进程逐渐拥有各自独立的内存空间。因为内存分配是以页为单位进行分配的,默认4k,如果父进程此时操作的是一个bigkey,重新申请大块内存耗时会变长,可能会产阻塞风险。另外,如果操作系统开启了内存大页机制(Huge Page,页面大小2M),那么父进程申请内存时阻塞的概率将会大大提高,所以在Redis机器上需要关闭Huge Page机制。
2.AOF 重写也有一个重写日志,为什么它不共享使用 AOF 本身的日志呢?
AOF重写不复用AOF本身的日志,一个原因是父子进程写同一个文件必然会产生竞争问题,控制竞争就意味着会影响父进程的性能。二是如果AOF重写过程中失败了,那么原本的AOF文件相当于被污染了,无法做恢复使用。所以Redis AOF重写一个新文件,重写失败的话,直接删除这个文件就好了,不会对原先的AOF文件产生影响。等重写完成之后,直接替换旧文件即可。
4.小结
通过上面对RDB和AOF两种redis数据持久化的阐述,我们可以看出,两者各有优劣。那么在我们的系统中应该怎么选择呢?
这里尽给出自己的一些建议:
- 数据不能丢失时,内存快照和 AOF 的混合使用是一个很好的选择;
- 如果允许分钟级别的数据丢失,可以只使用 RDB;
- 如果只用 AOF,优先使用 everysec 的配置选项,因为它在可靠性和性能之间取了一个平衡。