RDB文件的创建和载入
概述
Redis是一个键值对数据库服务器,服务器中通常包含着任意个非空数据库,而每个非空数据库中又可以包含任意个键值对,为了方便起见,将 服务器中的非空数据库 以及 它们的键值对 统称为 数据库状态
因为Redis是内存数据库,它将 自己的 数据库状态 储存在 内存里面,所以如果不想办法将储存在内存中的数据库状态保存到磁盘里面,那么一旦服务器进程退出,服务器中的数据库状态也会消失不见
为了解决这个问题,Redis提供了RDB持久化功能,这个功能可以将Redis在内存中的数据库状态保存到磁盘里面,避免数据意外丢失
RDB持久化既可以手动执行,也可以根据服务器配置选项定期执行,该功能可以将 某个时间点上的 数据库状态(所有的键值对) 保存到 一个RDB文件中
RDB持久化功能所生成的 RDB文件 是一个 经过压缩的二进制文件,通过该文件可以还原生成RDB文件时的数据库状态
因为RDB文件是保存在硬盘里面的,所以即使Redis服务器进程退出,甚至运行Redis服务器的计算机停机,但只要RDB文件仍然存在,Redis服务器就可以用它来还原数据库状态
RDB文件的创建
有两个Redis命令可以用于生成RDB文件,一个是SAVE
,另一个是BGSAVE
SAVE
命令 会阻塞 Redis服务器进程,直到RDB文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令请求:
redis> SAVE //等待直到RDB文件创建完毕
OK
SAVE命令复杂度:O(N)
和SAVE
命令直接阻塞服务器进程的做法不同,BGSAVE
命令会派生出一个子进程,然后 由子进程 负责创建RDB文件,服务器进程(父进程)继续处理命令请求
redis> BGSAVE //派生子进程,并由子进程创建RDB文件
Background saving started
BGSAVE命令复杂度:O(N),N为Redis服务器所有数据库包含的键值对总数量
创建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:
# 处理出错情况
handle_fork_error()
RDB文件的管理
RDB文件保存在dir配置指定的目录下,文件名通过dbfilename
配置指定。可以通过执行config set dir {newDir}
和config set dbfilename {newFileName}
运行期动态执行,当下次运行时RDB文件会保存到新目录
当遇到坏盘或磁盘写满等情况时,可以通过config set dir {newDir}
在线修改文件路径到可用的磁盘路径,之后执行bgsave进行磁盘切换,同样适用于AOF持久化文件
压缩:Redis默认采用LZF算法对生成的RDB文件做压缩处理,压缩后的文件远远小于内存大小,默认开启,可以通过参数config set rdbcompression {yes|no}
动态修改
虽然压缩RDB会消耗CPU,但可大幅降低文件的体积,方便保存到硬盘或通过网络发送给从节点,因此线上建议开启
校验:如果Redis加载损坏的RDB文件时拒绝启动,并打印如下日志:
# Short read or OOM loading DB. Unrecoverable error, aborting now.
这时可以使用Redis提供的redis-check-dump工具检测RDB文件并获取对应的错误报告
RDB文件的载入
和使用SAVE
命令或者BGSAVE
命令创建RDB文件不同,RDB文件的载入工作 是在 服务器启动时 自动执行的
所以Redis并没有专门用于载入RDB文件的命令,只要Redis服务器在启动时检测到RDB文件存在,它就会自动载入RDB文件
因为AOF文件的更新频率通常比RDB文件的更新频率高,所以:
- 如果服务器开启了AOF持久化功能,那么服务器会优先使用AOF文件来还原数据库状态
- 只有在AOF持久化功能处于关闭状态时,服务器才会使用RDB文件来还原数据库状态
服务器判断该用哪个文件来还原数据库状态的流程如下图所示:
载入RDB文件的实际工作由rdb.c/rdbLoad
函数完成,这个函数和rdbSave
函数之间的关系可以用下图表示:
RBD文件载入流程(重要!!!)
首先,当Redis服务器启动时,它会在工作目录中查找是否有RDB文件出现,如果有就打开它,然后读取文件的内容并执行以下载入操作:
- 检查文件开头的标识符是否为"REDIS",如果是则继续执行后续的载入操作,不是则抛出错误并终止载入操作
- 检查文件的RDB版本号,以此来判断当前Redis服务器能否读取这一版本的RDB文件
- 根据文件中记录的设备附加信息,执行相应的操作和设置
- 检查文件的数据库数据部分是否为空,如果不为空就执行以下子操作:
- 根据文件记录的数据库号码,切换至正确的数据库
- 根据文件记录的键值对总数量以及带有过期时间的键值对数量,设置数据库底层数据结构
- 一个接一个地载入文件记录的所有键值对数据,并在数据库中重建这些键值对
- 如果服务器启用了复制功能,那么将之前缓存的Lua脚本重新载入缓存中
- 遇到EOF标识,确认RDB正文已经全部读取完毕
- 载入RDB文件末尾记录的CRC64校验和,把它与载入数据期间计算出的CRC64校验和进行对比,以此来判断被载入的数据是否完好无损
- RDB文件载入完毕,服务器开始接受客户端请求
RDB文件载入时的服务器状态
服务器在载入RDB文件期间,会一直处于阻塞状态,直到载入工作完成为止
SAVE命令执行时的服务器状态
当SAVE
命令执行时,Redis服务器会被阻塞,所以当SAVE
命令正在执行时,客户端发送的所有命令请求都会被拒绝
只有在服务器执行完SAVE
命令、重新开始接受命令请求之后,客户端发送的命令才会被处理
BGSAVE命令执行时的服务器状态
因为BGSAVE
命令的保存工作是由子进程执行的,所以在子进程创建RDB文件的过程中,Redis服务器仍然可以继续处理客户端的命令请求
但是,在BGSAVE
命令执行期间,服务器处理SAVE
、BGSAVE
、BGREWRITEAOF
三个命令的方式会和平时有所不同
首先,在BGSAVE
命令执行期间,客户端发送的SAVE
命令会被服务器拒绝,服务器禁止SAVE
命令和BGSAVE
命令同时执行 是为了 避免父进程(服务器进程)和子进程同时执行两个rdbSave调用,防止产生竞争条件
其次,在BGSAVE
命令执行期间,客户端发送的BGSAVE
命令会被服务器拒绝,因为同时执行两个BGSAVE
命令也会产生竞争条件
最后,BGREWRITEAOF
和BGSAVE
两个命令不能同时执行:
- 如果
BGSAVE
命令正在执行,那么客户端发送的BGREWRITEAOF
命令 会被延迟到BGSAVE
命令执行完毕之后执行 - 如果
BGREWRITEAOF
命令正在执行,那么客户端发送的BGSAVE
命令会被服务器拒绝。因为BGREWRITEAOF
和BGSAVE
两个命令 的 实际工作 都由 子进程执行,所以这两个命令在操作方面并没有什么冲突的地方,不能同时执行它们只是一个性能方面的考虑,并发出两个子进程,并且这两个子进程都同时执行大量的磁盘写入操作,这怎么想都不会是一个好主意
BGSAVE运作流程
- 执行bgsave命令,Redis父进程判断 当前是否存在 正在执行的子进程,如RDB/AOF子进程,如果存在bgsave命令直接返回
- 父进程 执行fork操作 创建子进程,fork操作过程中父进程会阻塞,通过info stats命令查看
latest_fork_usec
选项,可以获取最近一个fork操作的耗时,单位为微秒 - 父进程fork完成后,bgsave命令返回“Background saving started”信息并不再阻塞父进程,可以继续响应其他命令
- 子进程创建RDB文件,根据父进程内存 生成 临时快照文件,完成后 对原有(rdb)文件 进行原子替换???。执行lastsave命令可以获取最后一次生成RDB的时间,对应info统计的
rdb_last_save_time
选项 - 进程发送信号给父进程表示完成,父进程更新统计信息
在运行fork()函数的时候,操作系统(类Unix操作系统)会使用写时复制(copy-on-write)策略
即fork()函数发生的一刻 父子进程 共享 同一内存数据,当父进程要更改其中某片数据时(如执行一条写命令),操作系统 会将该片数据 复制一份 以保证 子进程的数据不受影响
所以新的RDB文件 存储的是 运行fork()函数那一刻 的 内存数据
写时复制策略 也保证了 在运行fork()函数的时刻 虽然看上去 生成了两份内存副本,但实际上内存的占用空间并不会增加一倍。这就意味着当操作系统内存只有2 GB而Redis数据库的内存有1.5 GB时,运行fork()函数后内存占用空间并不会增加到3 GB(超出物理内存)
为此需要确保Linux操作系统允许应用程序申请超过可用内存(物理内存和交换分区)的空间,方法是在/etc/sysctl.conf
文件加入vm.overcommit_memory = 1
,然后重启系统或者执行sysctl vm.overcommit_memory=1
确保设置生效
另外需要注意的是,当进行快照的过程中,如果写入操作较多,造成运行fork()函数前后数据差异较大,会使得 内存使用空间 显著超过 实际数据大小,因为内存中不仅保存了当前的数据库数据,而且保存着运行fork()函数时刻的内存数据。进行内存占用空间估算时很容易忽略这一问题,造成内存占用空间超限
数据丢失(重要!!!)
RDB文件记录的是服务器在开始创建文件的那一刻,服务器中包含的所有键值对数据,这种数据持久化方式通常被称为时间点快照(point-in-time snapshot)
时间点快照持久化的一个特点是,系统在停机时将丢失最后一次成功实施持久化之后的所有数据
对于一个只使用RDB持久化的Redis服务器来说,服务器停机时 丢失的数据量 将取决于 最后一次成功执行的RDB持久化操作,以及该操作开始执行的时间
创建RDB文件的时间间隔越长,停机时丢失的数据也就越多
SAVE命令的停机情况
因为SAVE命令是一个同步操作,它的开始和结束都位于同一个原子时间之内,所以如果使用SAVE命令进行持久化,那么服务器在停机时 将丢失 最后一次成功执行SAVE命令之后 产生的所有数据
BGSAVE命令的停机情况
因为BGSAVE命令是一个异步命令,它的开始和结束并不位于同一个原子时间之内,所以如果使用BGSAVE命令进行持久化,那么服务器在停机时 丢失的数据量 将取决于 最后一次 成功执行 的BGSAVE命令 的 开始时间
RDB文件优缺点(重要!!!)
优点:
- RDB是一个紧凑压缩的二进制文件,代表Redis在某个时间点上的数据快照。非常适用于备份,全量复制等场景。比如每6小时执行bgsave备份,并把RDB文件拷贝到远程机器或者文件系统中(如hdfs),用于灾难恢复
- Redis加载RDB恢复数据远远快于AOF的方式
缺点:
- RDB方式数据没办法做到实时持久化/秒级持久化,因为bgsave每次运行都要执行fork操作创建子进程,属于重量级操作,频繁执行成本过高
- RDB文件使用特定二进制格式保存,Redis版本演进过程中有多个格式的RDB版本,存在老版本Redis服务无法兼容新版RDB格式的问题
- 针对RDB不适合实时持久化的问题,Redis提供了AOF持久化方式来解决