我们知道Redis是内存数据库,它把数据都存储在了内存中,如果Redis服务器出现了意外,比如宕机、断电等情况,那么内存中的数据就会全部丢失。所以必须有一种机制可以把内存中的数据保存到磁盘里面,为了解决这个问题,Redis提供了RDB和AOF两种持久化机制,这也是Redis的重要特性之一。
RDB全称Redis Database Backup file,也被称为Redis数据快照。
RDB持久化功能生成的RDB文件是一个经过压缩的二进制文件(默认为dump.rdb,也可以在redis.conf文件中修改),默认保存在当前的运行目录(也可以在redis.conf文件中修改),简单来说就是把内存中的数据都记录在磁盘上,当Redis出现意外,可以通过Redis重启加载该文件来恢复数据。
# 要备份数据库的文件名
dbfilename dump.rdb
?
# 工作目录。
# 数据库将会写在这个目录中,使用上面通过 ‘dbfilename’ 配置指令指定的文件名。同时,追加日志文件也 # 会在这个目录中创建。
# 请注意,这里需要指定一个目录,而不是一个文件名。
dir ./ ? #RDB和AOF目录
RDB持久化既可以手动执行,也可以根据服务器配置选项定期自动执行。
可以用SAVE和BGSAVE命令来手动生成RDB文件,但这两者有所不同。
SAVE命令会阻塞Redis进程,直到RDB文件创建完毕为止,在这之前,服务器不能处理任何命令请求。
SAVE <指定时间间隔> <执行指定次数更新操作>
ps:Redis的数据操作都是单线程。
BGSAVE命令会创建一个子进程来创建RDB文件,所以在创建过程中,Redis服务器仍然可以处理客户端的命令请求,但是会拒绝SAVE、BGSAVE和BGREWRITEAOF三个命令的执行。
??SAVE:为了避免父进程(服务器进程)和子进程同时执行两个rdbSave调用,防止产生竞态条件。
BGSAVE:也是防止产生竞态条件。
BGREWRITEAOF:此命令用于异步执行一个AOF重写操作,如果正在执行BGSAVE命令,那么客户端发送的BGREWRITEAOF命令会被延迟到BGSAVE命令执行完再执行。如果反过来,则会拒绝BGSAVE命令。因为它俩都是子进程在执行,而且这俩子进程都同时执行大量的磁盘写入操作,会影响服务性能。??
上面提到了两个命令SAVE和BGSAVE,SAVE是阻塞式的,因此其可用性欠佳,如果在数据量较少的情况下,基本上体会不到两个命令的差别,不过还是建议使用BGSAVE。
因为BGSAVE是非阻塞的,所以Redis允许通过配置redis.conf文件中的save配置,让服务器每隔一段时间自动执行一次BGSAVE命令,可以设置多个保存条件,比如以下3个默认条件(注释已翻译为中文):
# 将数据库保存到磁盘上:
# ? save <秒数> <更改数>
# ? 如果满足给定的秒数和对数据库的写操作次数,则保存数据库。
# ? 在下面的示例中,行为将是:
# ? 当至少有1个键发生更改时,每900秒(15分钟)保存一次
# ? 当至少有10个键发生更改时,每300秒(5分钟)保存一次
# ? 当至少有10000个键发生更改时,每60秒保存一次
# ? 注意:您可以通过注释掉所有的 "save" 行来完全禁用保存功能。
# ? 也可以通过添加一个只有一个空字符串参数的保存指令来删除所有先前配置的保存点,如下面的示例:
# ? save ""
save 900 1 ? #在900秒(15分钟)之内,对数据库进行了至少1次修改,则执行一次BGSAVE
save 300 10 ?#在300秒(5分钟)之内,对数据库进行了至少10次修改,则执行一次BGSAVE
save 60 10000 #在60秒之内,对数据库进行了至少10000次修改,则执行一次BGSAVE
服务器在恢复数据库数据(载入RDB文件)时,会一直处于阻塞状态,直到载入完成为止。
当Redis服务启动时,用户可以通过指定配置文件或者传入启动参数的方式设置save选项,如果没有主动设置,服务器就会使用redis.conf文件中默认的条件(上述3个条件)。
接着,服务器会根据save的选项所设置的保存条件,设置服务器状态redisServer结构的saveparams属性,除此之外,还有一个dirty计数器及lastsave属性:
struct redisServer{
? ?//....
? ?//记录了保存条件的数组
? ?struct saveparam *saveparams;
? ?//修改计数器
? ?long long dirty;
? ?//上一次执行保存的时间
? ?time_t lastsave;
? ?//.....
};
saveparams属性:是一个数组,每个saveparams结构都保存了一个save选项设置的保存条件:
struct saveparam{
? ?//秒数
? ?time_t seconds;
? ?//修改数
? ?int changes;
};
dirty属性:记录上一次成功成功执行SAVE命令或BGSAVE命令之后,服务器对数据库(全部数据库)进行了多少次修改(包括写入、删除、更新等操作)。
lastsave属性:是一个UNIX时间戳,记录服务器上一次成功执行SAVE或BGSAVE命令的时间。
那么它是如何被调用的呢?
Redis的服务器有一个周期性操作函数serverCron,它每隔100ms就会执行一次,该函数用于对正在运行的服务器进行维护,其中的一项工作就是检查save选项所设置的保存条件是否已满足,如果满足就执行BGSAVE命令。
AOF全称Append Only File,是Redis另外一种持久化机制,默认不开启(可在redis.conf文件中配置),它的目的是为了解决生成RDB文件后数据不能实时一致的问题,所以它采用日志的形式来记录每个写操作,并追加到文件中。Redis重启会根据日志文件的内容将写命令从前到后执行一遍来恢复数据。
# 默认情况下,Redis会异步将数据集转储到磁盘上。这种模式在许多应用中已经足够好了,但是如果 ? # Redis进程出现问题或者停电,可能会导致几分钟的写入丢失(取决于配置的保存点)。
# “追加模式文件”(Append Only File)是一种提供更好耐久性的备份模式。例如,在使用默认的数据
# 同步策略(在配置文件中稍后会提到)时,Redis在发生重大事件(如服务器停电)时可能会丢失一秒
# 钟的写入,或者如果Redis进程本身出现问题但操作系统仍在正常运行时,可能会丢失单个写入。
# 可以同时启用AOF持久化和RDB持久化,没有任何问题。如果在启动时启用AOF,Redis将加载AOF文件,
# 该文件具有更好的耐久性保证。
# 请访问 http://redis.io/topics/persistence 了解更多信息。
?
appendonly no #默认为no,开启改为yes
?
# 追加模式文件的名称是"appendonly.aof"(默认值)。
appendfilename "appendonly.aof"
ps:redis 7.0有个新特性Multi-part AOF,即把单个的AOF文件拆成了多个AOF文件,在MP-AOF中,有三种类型的AOF:
BASE:基础文件(只有一个)
appendonly.aof.1.base.rdb
INCR:增量文件(有多个)
appendonly.aof.1.incr.aof
appendonly.aof.2.incr.aof
mainfest:清单文件
appendonly.aof.mainfest
AOF持久化功能的实现可以分为命令追加(append)、文件写入、文件同步(sync)三个步骤:
- 服务器在执行完一个写命令后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾。aof缓冲区是redisServer结构体维护的一个SDS结构的属性。
struct redisServer{
? ?//....
? ?//AOF缓冲区
? ?sds aof_buf;
? ?//...
};
- 将aof缓冲区的数据写入到AOF文件,此时数据并没有写入到硬盘,而是拷贝到了内核缓冲区page cache(操作系统),等待内核将数据写入硬盘;具体内核缓冲区的数据什么时候写入到硬盘,由内核决定。
这个过程是将内核缓冲区中的数据写入到硬盘中的AOF文件中。如果由内核决定将内核数据写入硬盘的话,如果服务器宕机,那么就会丢失数据。为了解决这个问题,系统提供了fync和fdatasync两个同步函数,它们可以强制让操作系统立即将缓冲区中的数据写入到硬盘,以及三种策略:
- always:同步写回,每个写命令执行完立刻同步地将日志写回磁盘。(性能最差,最多丢失一个写指令的数据)
- everysec(默认):每秒执行一次。(是性能和数据安全性的折中方案,最多也就丢一秒的数据)
- no:根据操作系统和资源的情况,一定时间执行一次,时间不确定。(性能最好,可能会丢失上次同步AOF文件之后的所有写命令数据)
下面是redis.conf文件中的配置:
# fsync() 调用告诉操作系统实际上将数据写入磁盘,而不是等待输出缓冲区中的更多数据。有些操作系统会真的刷新数据到磁盘,而其他操作系统则会尽快尝试刷新数据。
# Redis 支持三种不同的模式:
# no: 不进行 fsync,只让操作系统在需要时刷新数据。更快。
# always: 每次写入追加日志后进行 fsync。较慢,但更安全。
# everysec: 每秒只进行一次 fsync。取折中。
# 默认是 "everysec",因为这通常是速度和数据安全之间的合适折衷。您需要理解是否可以将其放松到 "no",这允许操作系统在适当时刷新输出缓冲区,以获得更好的性能(但如果您可以接受某些数据丢失的想法,请考虑默认的持久化模式,即快照),或者相反,使用 "always",虽然非常慢,但比 "everysec" 稍微安全一些。
# 有关更多详细信息,请查阅以下文章:
# http://antirez.com/post/redis-persistence-demystified.html
# 如果不确定,可以使用 "everysec"。
?
# appendfsync always #同步
appendfsync everysec #每秒
# appendfsync no #由操作系统决定
因为AOF文件里包含了所有写命令,所以服务器只要读入并重新执行一遍AOF文件里面的命令,就可以还原服务器关闭之前的数据,详细步骤如下:
- 创建一个不带网络连接的伪客户端(fake client),因为Redis的命令只能在客户端上下文中执行,而载入AOF文件时所使用的命令直接来源于AOF文件而不是网络连接,所以服务器使用了一个没有网络连接的伪客户端来执行AOF文件保存的写命令,效果和带网络连接的客户端一样。
- 从AOF文件中分析并读取一条写命令。
- 使用伪客户端执行被读出的写命令。
- 重复执行2和3步骤,直到AOF文件中的所有写命令都被处理完毕为止。
完成以上步骤后,AOF文件所保存的数据就会被完整地还原出来。
因为AOF是通过保存被执行的写命令来持久化的,所以随着Redis的长时间运行,AOF的体积会越来越大,那么这就会带来以下几个问题:
- Linux文件系统对单文件的大小有限制,AOF文件过大的话无法保存。
- 文件过大,持久化追加写命令的时候效率也会很低。
- 数据载入(恢复)就更不用说了,它本身就是一条一条执行的,那就会导致执行的时间更久。
简而言之,AOF文件过大很可能对Redis服务器、甚至整个宿主机造成影响,所以Redis提供了重写的策略来应对这个问题。
先举个例子:
127.0.0.1:6379> rpush list "A" //["A"]
(integer) 1
127.0.0.1:6379> rpush list "B" "C" //["A","B","C"]
(integer) 3
127.0.0.1:6379> rpush list "D" "E" //["A","B","C","D","E"]
(integer) 5
127.0.0.1:6379> lpop list //["B","C","D","E"]
"A"
127.0.0.1:6379> lpop list //["C","D","E"]
"B"
我们可以看到,光是记录这个list键的数据就要在AOF文件中追加六条命令。如果在实际项目中,AOF中单单一个key键的持久化可能就得被追加成百上千次,想想就很恐怖。
不知道你有没有发现,尽管执行了这么多命令,而最终想要的数据就是当前Redis数据库存的值,所以重写的时候只用再读一遍数据库,把对应的写命令重写到AOF文件中,那保存的内容岂不是大大减少了。就像上述例子,重写的时候只需要把"rpush list "C" "D" "E""命令追加到AOF文件就行了(仅需一条)。实际上Redis也是这么做的。
Redis重写功能的大致流程如下
Redis 会启动一个 AOF 重写子进程,负责执行 AOF 重写操作。同时,Redis 会继续处理新的写入命令,并将这个写命令发送给AOF缓冲区和AOF重写缓冲区。
AOF缓冲区:无论重不重写都有这个缓冲区,AOF日志写入是AOF缓冲区->AOF文件。
AOF重写缓冲区:AOF重写子进程启动后开始使用。
子进程会按照一定的规则,读取当前数据库的键值对,并将其转化为合适的命令格式,保存到AOF重写缓冲区中。
子进程在遍历完整个数据库之后,就完成了重写工作。
接着,服务器父进程就会执行以下操作:
将AOF重写缓冲区中的所有内容写入到新的AOF文件中,使得新的AOF文件于当前数据库中的数据一致。
对新的AOF文件进行改名,原子地(atomic)覆盖现有的AOF文件,完成新旧两个AOF文件的替换。
最后,Redis 会关闭并销毁旧的 AOF 文件。
经过重写后的新的AOF文件只包含了恢复数据库数据所必须的命令,不会浪费任何空间。
ps:这么看来,AOF重写并未对旧的AOF文件进行任何读取、分析和写入操作,所以重写这个名称是有歧义的。
手动执行BGREWRITEAOF命令,开始重写aof文件:
127.0.0.1:6379> BGREWRITEAOF
Background append only file rewriting started
可以在redis.conf文件中修改对应的配置,让服务器自动执行BGREWRITEAOF命令。
# 自动重写只追加文件。
# 当 AOF 日志文件的大小增长到指定的百分比时,Redis 可以通过隐式调用 BGREWRITEAOF 来自动重写日志文件。
# 工作原理如下:Redis 在最新的重写之后记住 AOF 文件的大小(如果自重启以来没有发生重写,则使用启动时的 AOF 大小)。
# 将此基准大小与当前大小进行比较。如果当前大小大于指定的百分比,则会触发重写。此外,您需要指定重写 # AOF 文件的最小大小,这对于避免即使达到增加百分比,但仍非常小的情况下重写 AOF 文件非常有用。
# 指定百分之零的百分比以禁用自动 AOF 重写功能。
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb #表示触发AOF重写的最小文件体积,大于或等于64MB自动触发。
RDB和AOF在数据可靠性、性能、存储空间、使用场景等方面都有不同的优缺点,具体可以根据实际业务需求和硬件条件选择合适的持久化机制,或者同时使用两种持久化机制来实现更高的数据可靠性:
数据可靠性
RDB:可能会丢失最后一次快照之后的数据。如果RDB持久化过程中服务器宕机了,那么就会丢失这一次的数据。
AOF:可能会丢失最后一次(或一秒)写操作的数据。如果Redis刚刚执行完一个写命令,还没来得及写AOF文件就宕机了,那么就会丢失这一条数据【当然也得看它配置的策略,如果配置的是always(同步),那就丢一条,配置的everysec(每秒)那就会丢1秒的数据】,但它也比RDB更加靠谱一些。
性能:
RDB:备份和数据恢复比较快,适合做数据恢复。RDB存的是原生数据,所以直接加载到内存中即可。
AOF:写性能较高【RDB是对整个物理中的数据的快照,AOF则仅仅是记录每次写命令】,但数据恢复速度相对较慢【AOF需要对命令从头到尾再执行一次】。
存储空间:
RDB:二进制文件,体积较小。
AOF:文本文件,体积较大。
使用场景:
RDB:适用于需要定期备份、大规模数据恢复、恢复速度要求比较快的场景。
AOF:适用于对数据完整性要求较高、数据存档的场景。
既然都有各自的优缺点,那么它俩同时开启会怎样?
Redis 4.0之前,如果两种方式同时开启,dump.rdb和appendonly.aof文件都会生成,但在恢复数据时,会优先用appendonly.aof来恢复(比较完整),但AOF恢复数据相对较慢,如果Redis实例比较大的情况下,启动要花费很长时间。但再Redis 4.0后就优化了这个问题,这也是下面即将介绍的内容。
Redis 4.0为了解决上面的问题,带来了一个新的持久化选项——混合持久化。在开启混合持久化的情况下,AOF重写时会把Redis的持久化数据,以RDB的格式写入到AOF文件的开头,之后的数据再以AOF的格式追加到文件的末尾。
开启混合模式参数:
# 当重写 AOF 文件时,Redis能够在 AOF 文件中使用 RDB 前导以实现更快的重写和恢复。当开启此选项时,
# 重写后的 AOF 文件由两个不同的部分组成:
# [RDB 文件][AOF 尾部]
# 当加载 Redis 时,它会识别 AOF 文件以"REDIS"字符串开头,并加载带有前缀的 RDB 文件,然后继续加载 # AOF 的尾部。
aof-use-rdb-preamble yes
优缺点:
- 优点:混合持久化结合了RDB和AOF持久化的优点,开头为RDB格式,可以使Redis启动的更快,同时结合AOF的优点,又降低了大量数据丢失的风险。
- 缺点:在AOF文件中添加了RDB格式的内容,使得AOF文件的可读性变得很差;如果开始混合持久化,那么混合持久化的AOF是不能在旧版本中用的,不能向下兼容。
?End:希望对大家有所帮助,如果有纰漏或者更好的想法,请您一定不要吝啬你的赐教🙋。?