Redis是内存数据库,它将自己的数据库状态存储在内存里,所以为了之后重用数据(比如重启机器、机器故障之后恢复数据),或者是为了防止系统故障而将数据备份到一个远程位置,Redis提供持久化功能来解决这个问题。
目录
RDB持久化
Redis的第一种持久化方式叫快照(snapshotting,RDB),Redis可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。RDB持久化功能生成的是RDB文件是一个经过压缩的二进制文件,通过该文件可以还原创建生成RDB文件时的数据库状态。
Redis创建快照之后,可以对快照进行备份,可将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis主从结构,主要用来提高Redis性能),还可以将快照留在原地以便重启服务器的时候使用。快照持久化是Redis默认采用的持久化方式。
RDB文件的创建和载入
可以使用 SAVE 或 BGSAVE 命令来进行RDB文件的创建。
- SAVE:阻塞Redis服务器进程,直到RDB文件创建完成为止,在服务器进程阻塞期间,服务器不能处理任何命令请求。
- BGSAVE:派生出一个子进程,由这个子进程负责创建RDB文件,服务器进程(父进程)可以继续处理命令请求。
以下为bgsave的执行流程:
RDB文件的载入工作是在服务器启动时自动执行的,Redis没有专门用于载入RDB文件的命令。服务器在载入RDB文件期间,会一直处于阻塞状态,直到载入工作完成为止。
由于AOF文件的更新频率通常比RDB文件的更新频率高,如果服务器开启了AOF持久化功能,服务器会优先使用AOF文件来还原数据库状态;只有在AOF持久化功能处于关闭状态时,服务器才会使用RDB文件来还原数据库状态。
- 在BGSAVE命令执行期间,客户端发送的SAVE命令和BGSAVE命令都会被服务器拒绝,因为会产生竞争条件。
- 如果BGSAVE命令正在执行,客户端发送的BGREWRITEAOF命令会被延迟到BGSAVE命令执行完成之后执行。
- 如果BGREWRITEAOF命令正在执行,客户端发送的BGSAVE命令会被服务器拒绝。
自动间隔保存
由于SAVE会阻塞服务器而BGSAVE不会,所以Redis允许通过设置Redis服务器配置的 save 选项,可以让服务器每隔一段时间自动执行一次BGSAVE命令。可以配置多个规则,只要满足其中一个规则就执行BGSAVE命令。
save 选项默认配置为:
save 900 1 (在900秒之内,对数据库至少进行了一次修改,则执行BGSAVE命令)
save 300 10 (在300秒之内,对数据库进行了至少10次修改)
save 60 10000 (在60秒之内,对数据库进行了至少10000次修改)
服务器程序会根据 save 选项设置服务器状态redisServer结构的 saveparams 属性,saveparams 属性是一个数组,数组中每个元素都是一个saveparam结构:
Redis的save m n,是通过serverCron函数、dirty计数器、和lastsave时间戳来实现的。
- serverCron是Redis服务器的周期性操作函数,默认每隔100ms执行一次;该函数对服务器的状态进行维护,其中一项工作就是检查 save m n 配置的条件是否满足,如果满足就执行bgsave。
- dirty计数器是Redis服务器维持的一个状态,记录了上一次执行bgsave/save命令后,服务器状态进行了多少次修改(包括增删改);而当save/bgsave执行完成后,会将dirty重新置为0。(注意dirty记录的是服务器进行了多少次修改,而不是客户端执行了多少修改数据的命令;如果Redis执行了set mykey helloworld,则dirty值会+1;如果执行了sadd myset v1 v2 v3,则dirty值会+3)
- lastsave时间戳也是Redis服务器维持的一个状态(一个UNIX时间戳),记录的是上一次成功执行save/bgsave的时间。
RDB文件结构
RDB文件整体结构如下:
- REDIS:5字节,用于在载入文件时判断文件是否为RDB文件。
- db_version:4字节,字符串表示的整数,记录了RDB文件的版本号。
- databases:长度不定,0或任意多个数据库,以及各个数据库中的键值对数据。
- EOF:1字节,标志着RDB文件正文内容结束。
- check_num:8字节,无符号整数,保存一个检验和,根据前面四部分内容进行计算得到,用于检查RDB文件是否有出错或损坏的情况出现。
1. databases部分
一个RDB文件的databases部分可以保存任意多个非空数据库,如果0号数据库和3号数据库非空,则0号数据库和3号数据库的RDB文件结构示例:
其中每个非空数据库在RDB文件中的结构:
- SELECTDB:1字节,标志后面接着一个数据库号码。
- db_number:1、2、或5字节,保存一个数据库号码。
- key_value_pairs:长度不定,保存了数据库中所有的键值对数据,如果键值对带有过期时间,过期时间也会跟键值对保存在一起。
所以完整结构示例如下:
2. key_value_pairs部分
(1)不带过期时间的键值对结构:
TYPE:1字节,记录对象类型或底层编码,程序会根据这个常量的值来决定如何读入和解析value数据,值是以下常量中一种:
key和value分别保存键值对的键对象和值对象。
(2)带过期时间的键值对结构:
EXPIRETIME_MS:1字节,标志后面接着一个以毫秒为单位的过期时间。
ms:8字节,带符号整数,记录一个以毫秒为单位的UNIX时间戳。
3. value的编码
(1)字符串对象
TYPE:REDIS_RDB_TYPE_STRING
编码:REDIS_ENCODING_INT或者REDIS_ENCODING_RAW
<1>编码为REDIS_ENCODING_INT的字符串对象结构:
其中‘ENCODING‘的值可以是REDIS_RDB_ENC_INT8、REDIS_RDB_ENC_INT16或者REDIS_RDB_ENC_INT32。
<2>编码为REDIS_ENCODING_RAW的字符串对象
若服务器开启了了RDB文件压缩功能,则:
- 字符串长度 <= 20字节,原样保存。
- 字符串长度 > 20字节,压缩保存。
若服务器关闭了RDB文件压缩功能,则RDB程序总是原样保存字符串值。
可以通过配置文件的‘rdbcompression‘选项来设置RDB文件压缩功能是否开启。
原样保存的字符串结构:
len保存字符串长度,string则保存字符串值。
压缩保存的字符串结构:
REDIS_RDB_ENC_LZF:标志着字符串已经被LZF算法压缩过了。
compressed_len:记录字符串压缩后的长度。
origin_len:记录字符串原来的长度。
compressed_string:记录压缩后的字符串。
(2)列表对象
TYPE:REDIS_RDB_TYPE_LIST
编码:REDIS_ENCODING_LINKEDLIST
结构示例:
list_length:记录了列表的长度,即当前列表保存了多少个项。
因为每一个列表项都是一个字符串对象,所以读入程序会以处理字符串对象的方式来保存和读入列表项。
(3)集合对象
TYPE:REDIS_RDB_TYPE_SET
编码:REDIS_ENCODING_HT
结构示例:
set_size:记录集合大小,即当前集合保存了多少个元素。
因为每一个集合元素都是一个字符串对象,所以读入程序会以处理字符串对象的方式来保存和读入集合元素。
(4)哈希表对象
TYPE:REDIS_RDB_TYPE_HASH
编码:REDIS_ENCODING_HT
结构示例:
hash_size:记录哈希表大小,即当前哈希表保存了多少个键值对。
因为键值对的键和值都是字符串对象,所以读入程序会以处理字符串对象的方式来保存和读入键值对。
(5)有序集合对象
TYPE:REDIS_RDB_TYPE_ZSET
编码:REDIS_ENCODING_SKIPLIST
结构示例:
sorted_set_size:记录了有序集合的大小,即当前有序集合保存了多少个元素。每个元素分为成员(member)和分数(score)两部分,成员是一个字符串对象,分数是一个double浮点数(程序在保存RDB文件时会先将分值转换为字符串对象存储)。
(6)INTSET编码的集合
TYPE:REDIS_RDB_TYPE_SET_INTSET
保存时先将整数集合转换为字符串对象,然后再将这个字符串对象保存到RDB文件中;读取时则先读入字符串对象,再转换为整数集合对象。
(7)ZIPLIST编码的列表、哈希表或者有序集合
TYPE:REDIS_RDB_TYPE_LIST_ZIPLIST、REDIS_RDB_TYPE_HASH_ZIPLIST、REDIS_RDB_TYPE_ZSET_ZIPLIST
保存时先将压缩列表转换为一个字符串对象,再将这个字符串对象保存到RDB文件中;读取时则先读入字符串对象,再转换为压缩列表对象,最后根据TYPE的值,分别将压缩列表对象的类型设置为列表、哈希表或者有序集合。
总结
- RDB 文件用于保存和还原 Redis 服务器所有数据库中的所有键值对数据。
- SAVE 命令由服务器进程直接执行保存操作,所以该命令会阻塞服务器。
- BGSAVE 命令由子进程执行保存操作,所以该命令不会阻塞服务器。
- 服务器状态中会保存所有用
save
选项设置的保存条件,当任意一个保存条件被满足时,服务器会自动执行 BGSAVE 命令。 - RDB 文件是一个经过压缩的二进制文件,由多个部分组成。
- 对于不同类型的键值对, RDB 文件会使用不同的方式来保存它们。
AOF持久化
与RDB持久化通过保存数据库中的键值对来记录数据库的状态不同,AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的。
AOF持久化的实现
AOF 持久化功能的实现可以分为命令追加(append)、文件写入、文件同步(sync)三个步骤。
命令追加
当 AOF 持久化功能处于打开状态时, 服务器在执行完一个写命令之后, 会以协议格式将被执行的写命令追加到服务器状态的 aof_buf
缓冲区的末尾。
struct redisServer {
// ...
// AOF 缓冲区
sds aof_buf;
// ...
};
文件写入与同步
Redis 的服务器进程就是一个事件循环(loop), 这个循环中的文件事件负责接收客户端的命令请求, 以及向客户端发送命令回复, 而时间事件则负责执行像 serverCron
函数这样需要定时运行的函数。
因为服务器在处理文件事件时可能会执行写命令, 使得一些内容被追加到 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 文件, 如果上次同步 AOF 文件的时间距离现在超过一秒钟, 那么再次对 AOF 文件进行同步, 并且这个同步操作是由一个线程专门负责执行的。(默认选项) |
no | 将 aof_buf 缓冲区中的所有内容写入到 AOF 文件, 但并不对 AOF 文件进行同步, 何时同步由操作系统来决定。 |
AOF 持久化的效率和安全性
服务器配置 appendfsync
选项的值直接决定 AOF 持久化功能的效率和安全性。
- 当
appendfsync
的值为always
时, 服务器在每个事件循环都要将aof_buf
缓冲区中的所有内容写入到 AOF 文件, 并且同步 AOF 文件, 所以always
的效率是appendfsync
选项三个值当中最慢的一个, 但从安全性来说,always
也是最安全的, 因为即使出现故障停机, AOF 持久化也只会丢失一个事件循环中所产生的命令数据。- 当
appendfsync
的值为everysec
时, 服务器在每个事件循环都要将aof_buf
缓冲区中的所有内容写入到 AOF 文件, 并且每隔超过一秒就要在子线程中对 AOF 文件进行一次同步: 从效率上来讲,everysec
模式足够快, 并且就算出现故障停机, 数据库也只丢失一秒钟的命令数据。- 当
appendfsync
的值为no
时, 服务器在每个事件循环都要将aof_buf
缓冲区中的所有内容写入到 AOF 文件, 至于何时对 AOF 文件进行同步, 则由操作系统控制。因为处于no
模式下的flushAppendOnlyFile
调用无须执行同步操作, 所以该模式下的 AOF 文件写入速度总是最快的, 不过因为这种模式会在系统缓存中积累一段时间的写入数据, 所以该模式的单次同步时长通常是三种模式中时间最长的: 从平摊操作的角度来看,no
模式和everysec
模式的效率类似, 当出现故障停机时, 使用no
模式的服务器将丢失上次同步 AOF 文件之后的所有写命令数据。
AOF文件的载入与还原
因为AOF文件包含了重建数据库状态的所有写命令,所以服务器只需要读入并重新执行一遍AOF文件的写命令,就可以还原关闭之前的服务器状态。
由于Redis命令只能在客户端上下文执行,而载入AOF文件时使用的命令来源于AOF文件而不是网络连接,所以服务器使用了一个没有网络连接的伪客户端来执行AOF文件保存的命令。每从AOF文件分析并读取一条写命令,就用伪客户端执行此命令(放入事件执行队列,待事件完成后统一执行),一直重复至所以的写命令执行完毕。
AOF重写
由于AOF持久化是对命令的记录,所以必然会越来越多,内存越来越大,但其实很多命令是没必要记录的,为了解决这个问题,Redis提供了AOF文件重写的功能。
重写原理
AOF重写可以产生一个新的AOF文件,这个新的AOF文件和原有的AOF文件所保存的数据库状态一样,但体积更小。
AOF重写是一个有歧义的名字,该功能是通过读取数据库中的键值对来实现的,程序无须对现有AOF文件进行任何读入、分析或者写入操作;它的实际原理是不对现在的AOF文件进行任何操作,而是针对当前数据库状态中的键值对,对于每一个键使用一个增加命令来记录,这样对于所以键的所有AOF增加命令就组成了当前的数据库状态,省去了大量的其他命令(删,改等),大大减小了AOF文件的大小,新的AOF文件没有浪费任何内存空间。
需要注意的是,过期的键会被AOF重写命令所忽略,而带有过期时间的键,过期时间也要被重写。
后台重写
AOF重写如此大的写入时间阻塞显然是没法被服务器接受的,所以Redis使用子进程执行AOF重写程序。
对于上图的解释:
- 在重写期间,由于主进程依然在响应命令,为了保证最终备份的完整性;因此它依然会写入旧的AOF file中,如果重写失败,能够保证数据不丢失。
- 为了把重写期间响应的写入信息也写入到新的文件中,因此也会为子进程保留一个buf,防止新写的file丢失数据。
- 重写是直接把当前内存的数据生成对应命令,并不需要读取老的AOF文件进行分析、命令合并。
- AOF文件直接采用的文本协议,主要是兼容性好、追加方便、可读性高可认为修改修复。
- 在整个AOF重写过程中,只有信号处理函数(子进程重写完成后发送信号)执行时会对服务器(父进程)造成阻塞。
总结
- AOF 文件通过保存所有修改数据库的写命令请求来记录服务器的数据库状态。
- AOF 文件中的所有命令都以 Redis 命令请求协议的格式保存。
- 命令请求会先保存到 AOF 缓冲区里面, 之后再定期写入并同步到 AOF 文件。
appendfsync
选项的不同值对 AOF 持久化功能的安全性、以及 Redis 服务器的性能有很大的影响。- 服务器只要载入并重新执行保存在 AOF 文件中的命令, 就可以还原数据库本来的状态。
- AOF 重写可以产生一个新的 AOF 文件, 这个新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样, 但体积更小。
- AOF 重写是一个有歧义的名字, 该功能是通过读取数据库中的键值对来实现的, 程序无须对现有 AOF 文件进行任何读入、分析或者写入操作。
- 在执行 BGREWRITEAOF 命令时, Redis 服务器会维护一个 AOF 重写缓冲区, 该缓冲区会在子进程创建新 AOF 文件的期间, 记录服务器执行的所有写命令。 当子进程完成创建新 AOF 文件的工作之后, 服务器会将重写缓冲区中的所有内容追加到新 AOF 文件的末尾, 使得新旧两个 AOF 文件所保存的数据库状态一致。 最后,服务器用新的 AOF 文件替换旧的 AOF 文件, 以此来完成 AOF 文件重写操作。
参考文章:
《Redis设计与实现》