🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
# RDB [TOC=2,3] 在运行情况下,Redis 以数据结构的形式将数据维持在内存中,为了让这些数据在 Redis 重启之后仍然可用,Redis 分别提供了 RDB 和 AOF 两种[持久化](http://en.wikipedia.org/wiki/Persistence_\(computer_science\)) 模式。 在 Redis 运行时,RDB 程序将当前内存中的数据库快照保存到磁盘文件中,在 Redis 重启动时,RDB 程序可以通过载入 RDB 文件来还原数据库的状态。 RDB 功能最核心的是 `rdbSave` 和 `rdbLoad` 两个函数,前者用于生成 RDB 文件到磁盘,而后者则用于将 RDB 文件中的数据重新载入到内存中: ![digraph persistent { rankdir = LR; node [shape = circle, style = filled]; edge [style = bold]; redis_object [label = "内存中的\n数据对象", fillcolor = "#A8E270"]; rdb [label = "磁盘中的\nRDB文件", fillcolor = "#95BBE3"]; redis_object -> rdb [label = "rdbSave"]; rdb -> redis_object [label = "rdbLoad"];}](https://box.kancloud.cn/2015-09-13_55f4effe6ade7.svg) 本章先介绍 [SAVE](http://redis.readthedocs.org/en/latest/server/save.html#save "(in Redis 命令参考 v2.8)") 和 [BGSAVE](http://redis.readthedocs.org/en/latest/server/bgsave.html#bgsave "(in Redis 命令参考 v2.8)") 命令的实现,以及 `rdbSave` 和 `rdbLoad` 两个函数的运行机制,然后以图表的方式,分部分来介绍 RDB 文件的组织形式。 因为本章涉及 RDB 运行的相关机制,如果还没了解过 RDB 功能的话,请先阅读 [Redis 官网上的 persistence 手册](http://redis.io/topics/persistence) 。 ### 保存 `rdbSave` 函数负责将内存中的数据库数据以 RDB 格式保存到磁盘中,如果 RDB 文件已存在,那么新的 RDB 文件将替换已有的 RDB 文件。 在保存 RDB 文件期间,主进程会被阻塞,直到保存完成为止。 [SAVE](http://redis.readthedocs.org/en/latest/server/save.html#save "(in Redis 命令参考 v2.8)") 和 [BGSAVE](http://redis.readthedocs.org/en/latest/server/bgsave.html#bgsave "(in Redis 命令参考 v2.8)") 两个命令都会调用 `rdbSave` 函数,但它们调用的方式各有不同: - [SAVE](http://redis.readthedocs.org/en/latest/server/save.html#save "(in Redis 命令参考 v2.8)") 直接调用 `rdbSave` ,阻塞 Redis 主进程,直到保存完成为止。在主进程阻塞期间,服务器不能处理客户端的任何请求。 - [BGSAVE](http://redis.readthedocs.org/en/latest/server/bgsave.html#bgsave "(in Redis 命令参考 v2.8)") 则 `fork` 出一个子进程,子进程负责调用 `rdbSave` ,并在保存完成之后向主进程发送信号,通知保存已完成。因为 `rdbSave` 在子进程被调用,所以 Redis 服务器在 [BGSAVE](http://redis.readthedocs.org/en/latest/server/bgsave.html#bgsave "(in Redis 命令参考 v2.8)") 执行期间仍然可以继续处理客户端的请求。 通过伪代码来描述这两个命令,可以很容易地看出它们之间的区别: ~~~ def SAVE(): rdbSave() def BGSAVE(): pid = fork() if pid == 0: # 子进程保存 RDB rdbSave() elif pid > 0: # 父进程继续处理请求,并等待子进程的完成信号 handle_request() else: # pid == -1 # 处理 fork 错误 handle_fork_error() ~~~ ### SAVE 、 BGSAVE 、 AOF 写入和 BGREWRITEAOF 除了了解 RDB 文件的保存方式之外,我们可能还想知道,两个 RDB 保存命令能否同时使用?它们和 AOF 保存工作是否冲突? 本节就来解答这些问题。 ### SAVE 前面提到过,当 [SAVE](http://redis.readthedocs.org/en/latest/server/save.html#save "(in Redis 命令参考 v2.8)") 执行时,Redis 服务器是阻塞的,所以当 [SAVE](http://redis.readthedocs.org/en/latest/server/save.html#save "(in Redis 命令参考 v2.8)") 正在执行时,新的 [SAVE](http://redis.readthedocs.org/en/latest/server/save.html#save "(in Redis 命令参考 v2.8)") 、 [BGSAVE](http://redis.readthedocs.org/en/latest/server/bgsave.html#bgsave "(in Redis 命令参考 v2.8)") 或 [BGREWRITEAOF](http://redis.readthedocs.org/en/latest/server/bgrewriteaof.html#bgrewriteaof "(in Redis 命令参考 v2.8)") 调用都不会产生任何作用。 只有在上一个 [SAVE](http://redis.readthedocs.org/en/latest/server/save.html#save "(in Redis 命令参考 v2.8)") 执行完毕、Redis 重新开始接受请求之后,新的 [SAVE](http://redis.readthedocs.org/en/latest/server/save.html#save "(in Redis 命令参考 v2.8)") 、 [BGSAVE](http://redis.readthedocs.org/en/latest/server/bgsave.html#bgsave "(in Redis 命令参考 v2.8)") 或 [BGREWRITEAOF](http://redis.readthedocs.org/en/latest/server/bgrewriteaof.html#bgrewriteaof "(in Redis 命令参考 v2.8)") 命令才会被处理。 另外,因为 AOF 写入由后台线程完成,而 [BGREWRITEAOF](http://redis.readthedocs.org/en/latest/server/bgrewriteaof.html#bgrewriteaof "(in Redis 命令参考 v2.8)") 则由子进程完成,所以在 [SAVE](http://redis.readthedocs.org/en/latest/server/save.html#save "(in Redis 命令参考 v2.8)") 执行的过程中,AOF 写入和 [BGREWRITEAOF](http://redis.readthedocs.org/en/latest/server/bgrewriteaof.html#bgrewriteaof "(in Redis 命令参考 v2.8)") 可以同时进行。 ### BGSAVE 在执行 [SAVE](http://redis.readthedocs.org/en/latest/server/save.html#save "(in Redis 命令参考 v2.8)") 命令之前,服务器会检查 [BGSAVE](http://redis.readthedocs.org/en/latest/server/bgsave.html#bgsave "(in Redis 命令参考 v2.8)") 是否正在执行当中,如果是的话,服务器就不调用 `rdbSave` ,而是向客户端返回一个出错信息,告知在 [BGSAVE](http://redis.readthedocs.org/en/latest/server/bgsave.html#bgsave "(in Redis 命令参考 v2.8)") 执行期间,不能执行 [SAVE](http://redis.readthedocs.org/en/latest/server/save.html#save "(in Redis 命令参考 v2.8)") 。 这样做可以避免 [SAVE](http://redis.readthedocs.org/en/latest/server/save.html#save "(in Redis 命令参考 v2.8)") 和 [BGSAVE](http://redis.readthedocs.org/en/latest/server/bgsave.html#bgsave "(in Redis 命令参考 v2.8)") 调用的两个 `rdbSave` 交叉执行,造成竞争条件。 另一方面,当 [BGSAVE](http://redis.readthedocs.org/en/latest/server/bgsave.html#bgsave "(in Redis 命令参考 v2.8)") 正在执行时,调用新 [BGSAVE](http://redis.readthedocs.org/en/latest/server/bgsave.html#bgsave "(in Redis 命令参考 v2.8)") 命令的客户端会收到一个出错信息,告知 [BGSAVE](http://redis.readthedocs.org/en/latest/server/bgsave.html#bgsave "(in Redis 命令参考 v2.8)") 已经在执行当中。 [BGREWRITEAOF](http://redis.readthedocs.org/en/latest/server/bgrewriteaof.html#bgrewriteaof "(in Redis 命令参考 v2.8)") 和 [BGSAVE](http://redis.readthedocs.org/en/latest/server/bgsave.html#bgsave "(in Redis 命令参考 v2.8)") 不能同时执行: - 如果 [BGSAVE](http://redis.readthedocs.org/en/latest/server/bgsave.html#bgsave "(in Redis 命令参考 v2.8)") 正在执行,那么 [BGREWRITEAOF](http://redis.readthedocs.org/en/latest/server/bgrewriteaof.html#bgrewriteaof "(in Redis 命令参考 v2.8)") 的重写请求会被延迟到 [BGSAVE](http://redis.readthedocs.org/en/latest/server/bgsave.html#bgsave "(in Redis 命令参考 v2.8)") 执行完毕之后进行,执行 [BGREWRITEAOF](http://redis.readthedocs.org/en/latest/server/bgrewriteaof.html#bgrewriteaof "(in Redis 命令参考 v2.8)") 命令的客户端会收到请求被延迟的回复。 - 如果 [BGREWRITEAOF](http://redis.readthedocs.org/en/latest/server/bgrewriteaof.html#bgrewriteaof "(in Redis 命令参考 v2.8)") 正在执行,那么调用 [BGSAVE](http://redis.readthedocs.org/en/latest/server/bgsave.html#bgsave "(in Redis 命令参考 v2.8)") 的客户端将收到出错信息,表示这两个命令不能同时执行。 [BGREWRITEAOF](http://redis.readthedocs.org/en/latest/server/bgrewriteaof.html#bgrewriteaof "(in Redis 命令参考 v2.8)") 和 [BGSAVE](http://redis.readthedocs.org/en/latest/server/bgsave.html#bgsave "(in Redis 命令参考 v2.8)") 两个命令在操作方面并没有什么冲突的地方,不能同时执行它们只是一个性能方面的考虑:并发出两个子进程,并且两个子进程都同时进行大量的磁盘写入操作,这怎么想都不会是一个好主意。 ### 载入 当 Redis 服务器启动时,`rdbLoad` 函数就会被执行,它读取 RDB 文件,并将文件中的数据库数据载入到内存中。 在载入期间,服务器每载入 1000 个键就处理一次所有已到达的请求,不过只有 `PUBLISH` 、 `SUBSCRIBE` 、 `PSUBSCRIBE` 、 `UNSUBSCRIBE` 、 `PUNSUBSCRIBE` 五个命令的请求会被正确地处理,其他命令一律返回错误。等到载入完成之后,服务器才会开始正常处理所有命令。 Note 发布与订阅功能和其他数据库功能是完全隔离的,前者不写入也不读取数据库,所以在服务器载入期间,订阅与发布功能仍然可以正常使用,而不必担心对载入数据的完整性产生影响。 另外,因为 AOF 文件的保存频率通常要高于 RDB 文件保存的频率,所以一般来说,AOF 文件中的数据会比 RDB 文件中的数据要新。 因此,如果服务器在启动时,打开了 AOF 功能,那么程序优先使用 AOF 文件来还原数据。只有在 AOF 功能未打开的情况下,Redis 才会使用 RDB 文件来还原数据。 ### RDB 文件结构 前面介绍了保存和读取 RDB 文件的两个函数,现在,是时候介绍 RDB 文件本身了。 一个 RDB 文件可以分为以下几个部分: ~~~ +-------+-------------+-----------+-----------------+-----+-----------+ | REDIS | RDB-VERSION | SELECT-DB | KEY-VALUE-PAIRS | EOF | CHECK-SUM | +-------+-------------+-----------+-----------------+-----+-----------+ |<-------- DB-DATA ---------->| ~~~ 以下的几个小节将分别对这几个部分的保存和读入规则进行介绍。 ### REDIS 文件的最开头保存着 `REDIS` 五个字符,标识着一个 RDB 文件的开始。 在读入文件的时候,程序可以通过检查一个文件的前五个字节,来快速地判断该文件是否有可能是 RDB 文件。 ### RDB-VERSION 一个四字节长的以字符表示的整数,记录了该文件所使用的 RDB 版本号。 目前的 RDB 文件版本为 `0006` 。 因为不同版本的 RDB 文件互不兼容,所以在读入程序时,需要根据版本来选择不同的读入方式。 ### DB-DATA 这个部分在一个 RDB 文件中会出现任意多次,每个 `DB-DATA` 部分保存着服务器上一个非空数据库的所有数据。 ### SELECT-DB 这域保存着跟在后面的键值对所属的数据库号码。 在读入 RDB 文件时,程序会根据这个域的值来切换数据库,确保数据被还原到正确的数据库上。 ### KEY-VALUE-PAIRS 因为空的数据库不会被保存到 RDB 文件,所以这个部分至少会包含一个键值对的数据。 每个键值对的数据使用以下结构来保存: ~~~ +----------------------+---------------+-----+-------+ | OPTIONAL-EXPIRE-TIME | TYPE-OF-VALUE | KEY | VALUE | +----------------------+---------------+-----+-------+ ~~~ `OPTIONAL-EXPIRE-TIME` 域是可选的,如果键没有设置过期时间,那么这个域就不会出现;反之,如果这个域出现的话,那么它记录着键的过期时间,在当前版本的 RDB 中,过期时间是一个以毫秒为单位的 UNIX 时间戳。 `KEY` 域保存着键,格式和 `REDIS_ENCODING_RAW` 编码的字符串对象一样(见下文)。 `TYPE-OF-VALUE` 域记录着 `VALUE` 域的值所使用的编码,根据这个域的指示,程序会使用不同的方式来保存和读取 `VALUE` 的值。 Note 下文提到的编码在《[对象处理机制](#)》章节介绍过,如果忘记了可以回去重温下。 保存 `VALUE` 的详细格式如下: - `REDIS_ENCODING_INT` 编码的 `REDIS_STRING` 类型对象: 如果值可以表示为 `8` 位、 `16` 位或 `32` 位有符号整数,那么直接以整数类型的形式来保存它们: ~~~ +---------+ | integer | +---------+ ~~~ 比如说,整数 `8` 可以用 `8` 位序列 `00001000` 保存。 当读入这类值时,程序按指定的长度读入字节数据,然后将数据转换回整数类型。 另一方面,如果值不能被表示为最高 `32` 位的有符号整数,那么说明这是一个 `long long` 类型的值,在 RDB 文件中,这种类型的值以字符序列的形式保存。 一个字符序列由两部分组成: ~~~ +-----+---------+ | LEN | CONTENT | +-----+---------+ ~~~ 其中, `CONTENT` 域保存了字符内容,而 `LEN` 则保存了以字节为单位的字符长度。 当进行载入时,读入器先读入 `LEN` ,创建一个长度等于 `LEN` 的字符串对象,然后再从文件中读取 `LEN` 字节数据,并将这些数据设置为字符串对象的值。 - `REDIS_ENCODING_RAW` 编码的 `REDIS_STRING` 类型值有三种保存方式: 1. 如果值可以表示为 `8` 位、 `16` 位或 `32` 位长的有符号整数,那么用整数类型的形式来保存它们。 1. 如果字符串长度大于 `20` ,并且服务器开启了 [LZF 压缩功能](http://oldhome.schmorp.de/marc/liblzf.html) ,那么对字符串进行压缩,并保存压缩之后的数据。 经过 LZF 压缩的字符串会被保存为以下结构: ~~~ +----------+----------------+--------------------+ | LZF-FLAG | COMPRESSED-LEN | COMPRESSED-CONTENT | +----------+----------------+--------------------+ ~~~ `LZF-FLAG` 告知读入器,后面跟着的是被 LZF 算法压缩过的数据。 `COMPRESSED-CONTENT` 是被压缩后的数据, `COMPRESSED-LEN` 则是该数据的字节长度。 1. 在其他情况下,程序直接以普通字节序列的方式来保存字符串。比如说,对于一个长度为 `20` 字节的字符串,需要使用 `20` 字节的空间来保存它。 这种字符串被保存为以下结构: ~~~ +-----+---------+ | LEN | CONTENT | +-----+---------+ ~~~ `LEN` 为字符串的字节长度, `CONTENT` 为字符串。 当进行载入时,读入器先检测字符串保存的方式,再根据不同的保存方式,用不同的方法取出内容,并将内容保存到新建的字符串对象当中。 - `REDIS_ENCODING_LINKEDLIST` 编码的 `REDIS_LIST` 类型值保存为以下结构: ~~~ +-----------+--------------+--------------+-----+--------------+ | NODE-SIZE | NODE-VALUE-1 | NODE-VALUE-2 | ... | NODE-VALUE-N | +-----------+--------------+--------------+-----+--------------+ ~~~ 其中 `NODE-SIZE` 保存链表节点数量,后面跟着 `NODE-SIZE` 个节点值。节点值的保存方式和字符串的保存方式一样。 当进行载入时,读入器读取节点的数量,创建一个新的链表,然后一直执行以下步骤,直到指定节点数量满足为止: 1. 读取字符串表示的节点值 1. 将包含节点值的新节点添加到链表中 - `REDIS_ENCODING_HT` 编码的 `REDIS_SET` 类型值保存为以下结构: ~~~ +----------+-----------+-----------+-----+-----------+ | SET-SIZE | ELEMENT-1 | ELEMENT-2 | ... | ELEMENT-N | +----------+-----------+-----------+-----+-----------+ ~~~ `SET-SIZE` 记录了集合元素的数量,后面跟着多个元素值。元素值的保存方式和字符串的保存方式一样。 载入时,读入器先读入集合元素的数量 `SET-SIZE` ,再连续读入 `SET-SIZE` 个字符串,并将这些字符串作为新元素添加至新创建的集合。 - `REDIS_ENCODING_SKIPLIST` 编码的 `REDIS_ZSET` 类型值保存为以下结构: ~~~ +--------------+-------+---------+-------+---------+-----+-------+---------+ | ELEMENT-SIZE | MEB-1 | SCORE-1 | MEB-2 | SCORE-2 | ... | MEB-N | SCORE-N | +--------------+-------+---------+-------+---------+-----+-------+---------+ ~~~ 其中 `ELEMENT-SIZE` 为有序集元素的数量, `MEB-i` 为第 `i` 个有序集元素的成员, `SCORE-i` 为第 `i` 个有序集元素的分值。 当进行载入时,读入器读取有序集元素数量,创建一个新的有序集,然后一直执行以下步骤,直到指定元素数量满足为止: 1. 读入字符串形式保存的成员 `member` 1. 读入字符串形式保存的分值 `score` ,并将它转换为浮点数 1. 添加 `member` 为成员、 `score` 为分值的新元素到有序集 - `REDIS_ENCODING_HT` 编码的 `REDIS_HASH` 类型值保存为以下结构: ~~~ +-----------+-------+---------+-------+---------+-----+-------+---------+ | HASH-SIZE | KEY-1 | VALUE-1 | KEY-2 | VALUE-2 | ... | KEY-N | VALUE-N | +-----------+-------+---------+-------+---------+-----+-------+---------+ ~~~ `HASH-SIZE` 是哈希表包含的键值对的数量, `KEY-i` 和 `VALUE-i` 分别是哈希表的键和值。 载入时,程序先创建一个新的哈希表,然后读入 `HASH-SIZE` ,再执行以下步骤 `HASH-SIZE` 次: 1. 读入一个字符串 1. 再读入另一个字符串 1. 将第一个读入的字符串作为键,第二个读入的字符串作为值,插入到新建立的哈希中。 - `REDIS_LIST` 类型、 `REDIS_HASH` 类型和 `REDIS_ZSET` 类型都使用了 `REDIS_ENCODING_ZIPLIST` 编码, `ziplist` 在 RDB 中的保存方式如下: ~~~ +-----+---------+ | LEN | ZIPLIST | +-----+---------+ ~~~ 载入时,读入器先读入 `ziplist` 的字节长,再根据该字节长读入数据,最后将数据还原成一个 `ziplist` 。 - `REDIS_ENCODING_INTSET` 编码的 `REDIS_SET` 类型值保存为以下结构: ~~~ +-----+--------+ | LEN | INTSET | +-----+--------+ ~~~ 载入时,读入器先读入 `intset` 的字节长度,再根据长度读入数据,最后将数据还原成 `intset` 。 ### EOF 标志着数据库内容的结尾(不是文件的结尾),值为 `rdb.h/EDIS_RDB_OPCODE_EOF` (`255`)。 ### CHECK-SUM RDB 文件所有内容的校验和,一个 `uint_64t` 类型值。 REDIS 在写入 RDB 文件时将校验和保存在 RDB 文件的末尾,当读取时,根据它的值对内容进行校验。 如果这个域的值为 `0` ,那么表示 Redis 关闭了校验和功能。 ### 小结 - `rdbSave` 会将数据库数据保存到 RDB 文件,并在保存完成之前阻塞调用者。 - [SAVE](http://redis.readthedocs.org/en/latest/server/save.html#save "(in Redis 命令参考 v2.8)") 命令直接调用 `rdbSave` ,阻塞 Redis 主进程; [BGSAVE](http://redis.readthedocs.org/en/latest/server/bgsave.html#bgsave "(in Redis 命令参考 v2.8)") 用子进程调用 `rdbSave` ,主进程仍可继续处理命令请求。 - [SAVE](http://redis.readthedocs.org/en/latest/server/save.html#save "(in Redis 命令参考 v2.8)") 执行期间, AOF 写入可以在后台线程进行, [BGREWRITEAOF](http://redis.readthedocs.org/en/latest/server/bgrewriteaof.html#bgrewriteaof "(in Redis 命令参考 v2.8)") 可以在子进程进行,所以这三种操作可以同时进行。 - 为了避免产生竞争条件, [BGSAVE](http://redis.readthedocs.org/en/latest/server/bgsave.html#bgsave "(in Redis 命令参考 v2.8)") 执行时, [SAVE](http://redis.readthedocs.org/en/latest/server/save.html#save "(in Redis 命令参考 v2.8)") 命令不能执行。 - 为了避免性能问题, [BGSAVE](http://redis.readthedocs.org/en/latest/server/bgsave.html#bgsave "(in Redis 命令参考 v2.8)") 和 [BGREWRITEAOF](http://redis.readthedocs.org/en/latest/server/bgrewriteaof.html#bgrewriteaof "(in Redis 命令参考 v2.8)") 不能同时执行。 - 调用 `rdbLoad` 函数载入 RDB 文件时,不能进行任何和数据库相关的操作,不过订阅与发布方面的命令可以正常执行,因为它们和数据库不相关联。 - RDB 文件的组织方式如下: ~~~ +-------+-------------+-----------+-----------------+-----+-----------+ | REDIS | RDB-VERSION | SELECT-DB | KEY-VALUE-PAIRS | EOF | CHECK-SUM | +-------+-------------+-----------+-----------------+-----+-----------+ |<-------- DB-DATA ---------->| ~~~ - 键值对在 RDB 文件中的组织方式如下: ~~~ +----------------------+---------------+-----+-------+ | OPTIONAL-EXPIRE-TIME | TYPE-OF-VALUE | KEY | VALUE | +----------------------+---------------+-----+-------+ ~~~ RDB 文件使用不同的格式来保存不同类型的值。