Redis是一个基于内存中的数据结构存储系统,它所有的数据都存储在内存中。如果发生断电或者宕机,内存中的数据就会丢失。为了防止数据丢失,Redis提供了两种持久化的方案,一种是RDB(Redis DataBase),另一种是AOF(Append Only File)。
RDB持久化 RDB持久化指的是将某个时间点上的数据库状态保存到一个RDB文件中,在启动的时候,可以通过RDB文件将数据库状态还原回来。 rdb
RDB文件的创建和载入 有2个Redis命令可以生成RDB文件,一个是SAVE,另一个是BGSAVE。SAVE命令会阻塞Redis服务进程,直到RDB文件创建完成为止,在此期间,客户端所有命令请求都会被阻塞。而BGSAVE命令会派生出一个子进程,由子进程负责创建RDB文件,服务器进程继续处理命令请求。当然,在BGSAVE期间,服务器处理SAVE、BGSAVE和BGREWRITEAOP三个命令的方式会有所不同。具体来说就是,在BGSAVE期间,服务器会拒绝执行SAVE、BGSAVE命令(防止产出竞争);对于BGREWRITEAOP命令,服务器会延迟到BGSAVE执行完成才真正开始执行。
创建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_sifnal() else: # 处理出错 handle_fork_error() 复制代码 由fork创建的新进程被称为子进程(child process)。该函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是0,而父进程的返回值则是新进程(子进程)的进程id。fork之后,操作系统会复制一个与父进程完全相同的子进程,虽说是父子关系,但是在操作系统看来,他们更像兄弟关系,这2个进程共享代码空间,但是数据空间是互相独立的,子进程数据空间中的内容是父进程的完整拷贝,指令指针也完全相同,子进程拥有父进程当前运行到的位置。具体可参考fork出的子进程和父进程。
RDB文件载入是在Redis启动的时候自动执行的。在服务只开启RDB持久化时,只要在启动的时候检测到RDB文件,就会执行自动载入。在RDB文件载入期间,服务器会一直处理阻塞状态,直到载入完成为止。
自动间隔保存 除了手动执行SAVE或者BGSAVE命令来创建RDB文件,Redis还支持通过设置服务器配置的save选项,让服务器每隔一段时间自动执行BGSAVE命令。
用户可以设置多个保存条件,只要其中一个条件被满足,服务器就会执行BGSAVE命令。例如:
save 900 1 save 300 10 save 60 10000 复制代码 那么满足以下任意一个条件,BGSAVE命令就会被执行:
服务器在900秒之内,对数据库进行了至少1次修改。 服务器在300秒之内,对数据库进行了至少10次修改。 服务器在60秒之内,对数据库进行了至少10000次修改。 RDB文件结构 一个完整RDB文件包含的各个部分如下图所示: rdbfile
为了方便区分变量、数据和常量,本文用全大写单词表示常量,用全小写单词表示变量和数据。
数据部分 数据类型 长度 数据含义 REDIS 常量 5字节 RDB文件标识,用来快速检查载入的文件是否是RDB文件 db_version 变量 4字节 RDB文件版本号 database 数据 不定 Redis各个非空数据库状态 EOF 常量 1字节 标志着RDB文件正文内容结束 check_sum 变量 8字节 校验和,根据前面4部分计算而来,用来检查RDB文件完整性 database部分 database部分保存了任意多个非空数据库状态,对于每一个非空数据库,database结构如下: rdb-database
数据部分 数据类型 长度 数据含义 SELECT_DB 常量 1字节 数据库开头标识 db_number 变量 1-5个字节 数据库开头号码 key_value_pairs 数据 不定 数据库所有键值对数据 key_value_pairs部分 key_value_pairs部分保存了一个数据所有的键值对数据,其中不带过期时间的键值对有TYPE、key、value三部分组成,带过期时间的话,会在前面多出EXPIRETIME_MS和ms两部分。 rdb-kv
数据部分 数据类型 长度 数据含义 EXPIRETIME_MS 常量 1字节 键值对过期时间标识 ms 变量 8字节 键值对的过期时间(毫秒) TYPE 常量 1字节 数据库值的类型 key 变量 不定 数据库键,永远是字符串对象 value 变量 不定 数据库值,根据TYPE不用,value保存结构也不同 每个value都保存着一个值对象,每个值对象的类型由TYPE字段记录。根据TYPE类型不同,value部分的结构、长度也会有所不同。这块思想上跟内存中底层数据结构类似,这里就不展开细讲了。
至此,一个RDB文件完整的部分就出来了,如下所示: rdb-all
AOF持久化 除了RDB持久化之外,Redis还支持AOF持久化功能。AOF持久化是通过保存Redis服务器执行的写命令来记录数据库状态的。 aof
被写入AOF文件的所有命令都是以Redis的命令请求协议格式保存的,因为Redis的命令请求协议格式是纯文本格式。服务器在启动的时候,可以通过载入并重放AOF文件的命令来还原数据库状态。
AOF持久化的实现 AOF持久化实现可以分为以下三个步骤:
命令追加 文件写入 文件同步(刷盘) 命令追加 当启用AOF持久化的时候,服务器在执行完一个写命令之后,会将该命令追加到aof_buf缓存区的末尾。
struct redisServer { // ...
// AOF缓冲区
sds aof_buf;
// ...
} 复制代码 AOF文件的写入与同步 Redis服务器进程是一个事件循环,循环中的文件事件负责接收客户端的命令请求和发送命令回复,而时间事件则负责执行像serverCorn函数这样需要定时运行的函数。因为处理文件事件会包含写命令,使得一些内容追加到aof_buf缓冲区中,所以在事件循环结束前还需要调用flushAppendOnlyFile函数,决定是否需要将aof_buf缓冲区的内容写入并同步到AOF文件中。伪代码如下:
def eventLoop(): while True:
# 处理文件事件,接收命令请求和发送命令回复
# 将写命令追加到aof_buf缓存区中
processFileEvents()
# 处理时间事件
processTimeEvents()
# 将aof_buf缓存区写入并同步在AOF文件中
flushAppendOnlyFile()
复制代码 flushAppendOnlyFile函数由配置项appendfsync决定:
appendfsync的值 flushAppendOnlyFile函数行为 always 每次都将aof_buf缓冲区数据写入并同步到AOF文件中 everysec(默认) 每次都将aof_buf缓冲区数据写入AOF文件中,但是每隔1秒进行进行AOF文件同步 no 每次都将aof_buf缓冲区数据写入AOF文件中,文件同步完全由操作系统控制 AOF文件载入与数据还原 因为AOF文件中包含了所有的写命令,所以在服务器启动的时候,只需要载入并重放AOF文件的命令就能够恢复到数据库原来的状态。具体步骤如下:
创建一个不带网络连接的伪客户端(fake client)。 从AOF文件中读出一条写命令。 使用伪客户端执行命令。 一直执行步骤二和三,直到AOF文件中的所有命令都执行完。 AOF重写 因为AOF持久化是通过追加命令的方式实现的,所以随着服务器运行,AOF文件体积也会越来越大。如果不加以控制,不仅会对整个Redis服务器造成不好的影响,而且还会导致AOF命令重放时间过长。为了解决AOF文件体积膨胀的问题,Redis支持了AOF文件重写功能。通过该功能,可以实现创建一个体积小的多的AOF文件来代替现有的AOF文件。
AOF重写的实现 虽然叫AOF重写,但实际上并不是对现有的AOF文件进行读取、分析和重新写入。实际上,AOF重写是通过读取服务器当前数据库状态来实现的。首先从数据库中读取现有的键值,然后用一条命令去记录键值对,代替之前记录的针对该键的多条命令,这就是AOF重写的原理。
AOF后台重写 AOF重写需要遍历整个数据库并把所有键值对都以命令的形式记录下来,很明显,这是一个非常耗费时间的事情。如果有服务器进程直接执行AOF重写,那么整个服务器将会被阻塞,这对于Redis来说显然是不能接受的。因此,Redis支持AOF后台重写功能,具体来讲就是由子进程处理AOF重写。
不过,AOF后台重写也有一个问题需要解决。因为在AOF后台重写期间,主进程仍然可以处理写命令,新的命令仍然会改变现有的数据库状态,最终就会导致AOF后台重写的文件保存的数据库状态和当前的数据库状态不一致。
为了解决上述的数据不一致的问题,Redis服务器设置了一个AOF重写缓存区。这个缓存区在创建子进程之后开始使用,此时Redis服务器执行完写命令之后,会同时将这个命令发送到AOF缓冲区和AOF重写缓存区。 aof-rewrite
这么做就能保证:
AOF缓冲区的内容会正常写入和同步到现有AOF文件中,现有AOF文件依然可以正常处理。 AOF后台重写期间,所有新加入的写命令都会保存在AOF重写缓存区中。再AOF后台重写完成之后,再将AOF重写缓存区的内容追加到新的AOF文件中即可,最后再用新的AOF文件替换之前的AOF文件。