
深入Redis系列(三)redis持久化之RDB快照持久化和AOF日志持久化
本文内容参考《redis设计与实现》一书总结归纳而得,归于合集: redis知识汇总
因为Redis是内存数据库,它将自己的数据库状态储存在内存里面,所以如果不想办法将储存在内存中的数据库状态保存到磁盘里面,那么一旦服务器进程退出,服务器中的数据库状态也会消失不见。
持久化的方式有2种
1.快照 rdb
2.日志 aof
01 RDB持久化
RDB是一种快照文件(二进制文件),
RDB持久化既可以手动执行,也可以根据服务器配置选项定期执行,该功能可以将某个时间点上的数据库状态保存到一个RDB文件中。RDB 持久化功能所生成的
RDB文件是一个经过压缩的二进制文件,通过该文件可以还原生成RDB文件时的数据库状态。

有两个Redis命令可以用于生成RDB文件,一个是SAVE,另一个是BGSAVE(BackGround save)。
SAVE命令会阻塞Redis服务器进程,直到RDB文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令请求:
#等待直到RDB文件创建完毕redis> SAVE OK
和SAVE命令直接阻塞服务器进程的做法不同,BGSAVE命令会派生出一个子进程,然后由子进程负责创建RDB文件,服务器进程(父进程)继续处理命令请求:
# 派生子进程,并由子进程创建RDB文件
redis> BGSAVEBackground saving started
创建RDB文件的实际工作由rdb.c/rdbSave函数完成,SAVE命令和BGSAVE命令会以不同的方式调用这个函数,通过以下伪代码可以明显地看出这两个命令之间的区别:
def save():
# 创建RDB文件 rdbSave();
def BGSAVE():
# 创建子进程 pid = fork()
if pid == 0:
# 子进程负责创建RDB文件 rdbSave()
# 完成之后向父进程发送信号 signal_parent()
elif pid > 0:
# 父进程继续处理命令请求,并通过轮询等待子进程的信号 handle_request_and_wait_signal()else: else:
# 处理出错 handle_fork_error()
bgsave是异步的,他会使用Linux的fork()函数,这个函数作用是生成子进程,让子进程完成RDB快照的生成保存。此时客户端执行的其他命令的执行不会被阻塞(阻塞发生在fork)。当rdb快照生成以后,子进程会发送成功信号给主进程,让主进程关掉子进程。
除了要知道bgsave要用到fork之外,还要知道用到了cow(copy-on-
write,写时复制),这意味着redis在fork出一个子进程之后,不会马上给子进程分配新的内存空间,而是和父进程共享同一个物理空间,指针指向的是同一块物理空间。
只有在bgsave过程中,父进程发生写操作修改内存数据时,才会真正去
分配内存空间,并复制内存数据,而且也只是复制被修改的内存页中的数据,并不是全部内存数据;
举个例子:
主进程和子进程现在都指向同一块内存空间,这个内存空间是由一块块小的内存块组成的,假设一共有10000块。
当主进程在bgsave过程中写入新数据set k1 v1,
此时会开辟一个新的内存空间(现在就有10001块内存块了),新的key就存在这第10001个内存块中。主进程就一共指向了10001个内存块,而子进程还是只用到原来的10000个内存块。


save和bgsave的区别:
1.save同步,bgsave异步;
2.save阻塞客户端命令,bgsave会fork子进程并行复制,不阻塞客户端命令;
3.save不会额外消耗内存,bgsave会需要fork,额外消耗内存;
和使用SAVE命令或者BGSAVE命令创建RDB文件不同,RDB文件的载人工作是在服务器启动时自动执行的,所以Redis并没有专门用于载入RDB文件的命令,只要Redis服务器在启动时检测到RDB文件存在,它就会自动载入RDB文件。
$ redis-server[7379] 30 Aug 21:07:01.270
# Server started,Redis version 2.9.11 [7379] 30 Aug 21:07:01.289 * DB loaded from disk:0.018 seconds[7379] 30 Aug 21:07:01.289 * The server is now ready to accept connections on port 6379
另外值得一提的是,因为AOF文件的更新频率通常比RDB文件的更新频率高,所以
如果服务器开启了AOF持久化功能,那么服务器会优先使用AOF文件来还原数据库状态。
只有在AOF持久化功能处于关闭状态时,服务器才会使用RDB文件来还原数据库状态。
服务器判断该用哪个文件来还原数据库状态的流程如图所示。

自动间隔性保存
因为BGSAVE命令可以在不阻塞服务器进程的情况下执行,所以Redis允许用户通过设置服务器配置的save选项,让服务器每隔一段时间自动执行一次BGSAVE命令。
用户可以通过save选项设置多个保存条件,但只要其中任意一个条件被满足,服务器就会执行BGSAVE命令。
举个例子,如果我们向服务器提供以下配置:
save 900 1 save 300 10 save 60 10000
那么只要满足以下三个条件中的任意一个,BGSAVE命令就会被执行:
服务器在 900秒之内,对数据库进行了至少1次修改。
服务器在300秒之内,对数据库进行了至少10次修改。
服务器在60秒之内,对数据库进行了至少10000次修改。
① 设置保存条件
当Redis服务器启动时,用户可以通过指定配置文件或者传入启动参数的方式设置save
选项,如果用户没有主动设置save选项,那么服务器会为save选项设置默认条件:
save 900 1 save 300 10 save 60 10000
接着,服务器程序会根据save选项所设置的保存条件,设置服务器状态 redisServer 结构的saveparams属性:
struct redisServer { // ... struct saveparam *saveparams; // ...};
saveparam结构定义为:
struct saveparam { // 秒数 time_t seconds; // 修改数 int changes;};

② dirty计数器和lastsave属性
dirty计数器记录距离上一次成功执行SAVE命令或者BGSAVE命令之后,服务器对数据库状态(服务器中的所有数据库)进行了多少次修改(包括写入、删除、更新等操作)。
lastsave 属性是一个UNIX时间戳,记录了服务器上一次成功执行SAVE命令或者BGSAVE命令的时间。
struct redisServer (
// 修改计数器
long long dirty;
// 上一次执行保存的时间
time_t lastsave;
);
③ 检查保存条件是否满足
Redis
的服务器周期性操作函数serverCron默认每隔100毫秒就会执行一次,该函数用于对正在运行的服务器进行维护,它的其中一项工作就是检查save选项所设置的保存条件是否已经满足,如果满足的话,就执行BGSAVE命令。
RDB文件结构

REDIS:RDB文件的最开头是REDIS部分,这个部分的长度为5字节,保存着”REDIS”五个字符。通过这五个字符,程序可以在载入文件时,快速检查所载入的文件是否RDB文件。
db_version:四字节,记录版本号。
databases:databases部分包含着零个或任意多个数据库,以及各个数据库中的键值对数据
如果服务器的数据库状态为空(所有数据库都是空的),那么这个部分也为空,长度为0字节。
如果服务器的数据库状态为非空(有至少一个数据库非空),那么这个部分也为非空,根据数据库所保存键值对的数量、类型和内容不同,这个部分的长度也会有所不同。
EOF:1字节,表示RDB文件的正文内容结束。
check_sum:8字节无符号整数,保存校验和。
① databases
一个RDB文件的databases部分可以保存任意多个非空数据库。
例如,如果服务器的0号数据库和3号数据库非空,那么服务器将创建一个如图10-12所示的RDB文件,图中的database
0代表0号数据库中的所有键值对数据,而database 3则代表3号数据库中的所有键值对数据。

每个非空数据库在RDB文件中都可以保存为SELECTDB、db_number、key_value_pairs三个部分,如图10-13所示。

SELECTDB:1字节的常量,表示后面将要跟上数据库的号码
db_number:数据库的号码,当程序读入db
number部分之后,服务器会调用SELECT命令,根据读入的数据库号码进行数据库切换,使得之后读入的键值对可以载入到正确的数据库中。
key_value
pairs部分保存了数据库中的所有键值对数据,如果键值对带有过期时间,那么过期时间也会和键值对保存在一起。根据键值对的数量、类型、内容以及是否有过期时间等条件的不同,key
value pairs 部分的长度也会有所不同。
示例

② key_value_paires
RDB文件中的每个key_value_pairs 部分都保存了一个或以上数量的键值对,如果键值对带有过期时间的话,那么键值对的过期时间也会被保存在内。
不带过期时间的键值对在RDB文件中由TYPE、key、value三部分组成。

RDB的配置
RDB在配置文件的相关rdb配置:
dbfilename dump.rdb
# rdb文件名dir ./
# rdb文件存放位置stop-writes-on-bgsave-error yes
# bgsave发生错误时是否停止rdbcompression yes
# 是否对rdb压缩rdbchecksum yes
# 重启时是否检查rdb文件是否正确
RDB的生成触发时机
a.按配置文件所配置的时间间隔自动触发生成
b.全量复制时触发
进行主从复制时,主会自动生成rdb
c.执行debug reload时触发
这是一个重启命令,使用该命令重启时,内存的数据不会清空,并且会生成rdb
d.执行shutdown save关闭服务器时触发
RDB优缺点
优点
RDB文件是某个时间节点的快照,默认使用LZF算法进行压缩,压缩后的文件体积远远小于内存大小,适用于备份、全量复制等场景;
Redis加载RDB文件恢复数据要远远快于AOF方式;
缺点
RDB方式实时性不够,无法做到秒级的持久化;
每次调用bgsave都需要fork子进程,fork子进程属于重量级操作,频繁执行成本较高;
RDB文件是二进制的,没有可读性,AOF文件在了解其结构的情况下可以手动修改或者补全;
版本兼容RDB文件问题;
02 AOF持久化
除了RDB持久化功能之外,Redis还提供了AOF(Append Only
File)持久化功能。与RDB持久化通过保存数据库中的键值对来记录数据库状态不同,AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的,如图11-1所示。
AOF持久化的实现
① 命令追加
当AOF持久化功能处于打开状态时,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾:
struct redisServer { sds aof_buf;}
例如:执行了下面三个命令:
SADD databases "Redis""MongoDB""MariaDB"SET date"2013-9-5"INCR click_counter 10086
那么缓冲区将包含这三个命令的协议内容:

② AOF文件的写入与同步
因为服务器在处理文件事件时可能会执行写命令,使得一些内容被追加到aof_buf缓冲区里面,所以在服务器每次结束一个事件循环之前,它都会调用flushAppendOnlyFile函数,考虑是否需要将aof_buf缓冲区中的内容写入和保存到AOF文件里面,这个过程可以用以下伪代码表示:
def eventLoop(): while True: #处理文件事件,接收命令请求以及发送命令回复 #处理命令请求时可能会有新内容被追加到aof_buf缓冲区中 processFileEvents()
#处理时间事件 processTimeEvents()
#考虑是否要将aof_buf中的内容写入和保存到AOF文件里面 flushAppendOnlyFile()
flushAppendOnlyFile函数的行为由服务器配置的appendfsync选项的值来决定,各个不同值产生的行为如表11-1所示。
AOF的三种策略
always
每执行一条命令会立刻写入到aof
优点:不丢失数据
缺点:IO开销大
everysec
每秒执行一次写入aof
优点:IO开销没那么大
缺点:会丢1s的数据
no
由系统决定
优点:由操作系统自己决定,不用开发者操心
缺点:丢失的数据不可控
默认 everysec
AOF文件的载入与数据还原
因为AOF文件里面包含了重建数据库状态所需的所有写命令,所以服务器只要读入并重新执行一遍AOF文件里面保存的写命令,就可以还原服务器关闭之前的数据库状态。

AOF的重写
随着时间的推移,并发,执行的命令累积,aof文件会变的很大,造成的问题是使用aof恢复数据会很慢
解决方法:AOF重写
把过期的,没有用的,重复的命令化简,使得aof文件变小。
作用有二:减小磁盘占用,加快恢复数据速度
例如:
incr num # 执行一亿次,会有一亿条命令
aof会简化为 set num 100000000
AOF重写的两种方式:
a.手动执行bgrewriteaof
原理和bgsave相似:client执行bgrewrite,redis会fork一个子进程进行aof重写,这里的重写不是读取aof文件再简化里面的命令,而是对内存的一个回溯,根据内存数据生成一个aof新文件。
在这个过程中redis主进程会正常执行客户端其他写操作,并将这些操作写入aof_rewrite_buf这个缓冲,再从这个缓冲写入到新aof中。最后新aof替代旧的aof文件。

aof appendfsync(过程1) 和 aof rewrite(过程2)的流程图
b.根据配置文件自动触发
auto-aof-rewrite-min-size
# aof文件达到指定大小就会重写auto-aof-rewrite-percentage
# aof文件增长率达到指定值就会重写
必须两个条件同时触发才会重写。
当进行aof持久化(aof appendfsync)时会检测这两个条件,两个条件满足就会自动执行bgrewriteaof命令触发aof重写。
不要搞混了aof持久化(aof
appendfsync)和aof重写,这是两个不同的独立的过程。但是他们一般会同时进行(子进程在执行aof重写的时候,父进程在产生新命令的aof日志)
AOF重写的注意事项
BGREWRITEAOF和BGSAVE两个命令不能同时执行,不能同时执行它们只是一个性能方面的考虑——并发出两个子进程,并且这两个子进程都同时执行大量的磁盘写入操作,磁盘开销大了还在其次,子进程占用内存可能发生内存不足的情况。
AOF的配置
appendonly yes
# 开启aof持久化appendfilename "appendonly-${port}.aof"appendfsync everysec dir /bigdiskpathno-appendfsync-on-rewrite yes
# 表示在进行aof重写时,停止命令写入(旧的)aof文件。可以节省性能。但是如果这段时间aof重写失败,旧的aof也没有更新命令,可能会丢失数据。需要作出权衡auto-aof-rewrite-min-size 100auto-aof-rewrite-percentage 64m
如果redis已经启动,可以使用 config set 动态配置。
AOF和RDB对比:
命令 RDB AOF启动优先级 低 高体积 小 大恢复速度 块 慢数据安全 丢数据 根据策略轻重 重 轻