RDB文件格式

  • 一、Redis RDB文件
  • 二、解析RDB的高级算法
  • 2.1 Magic Number
  • 2.2 RDB 版本号
  • 2.3 操作码
  • 2.3.1 数据库选择器
  • 2.3.2 Resizeb信息
  • 2.3.3 辅助字段
  • 2.3.4 键值对
  • key 到期时间戳
  • 值类型
  • 2.4 CRC64校验码
  • 三、编码方式
  • 3.1 Length Encoding 长度编码
  • 3.2 字符串编码
  • 3.2.1 长度前缀字符串
  • 3.2.2 整数作为字符串
  • 3.2.3 压缩字符串
  • 3.3 列表编码
  • 3.4 集合编码
  • 3.5 有序集合编码
  • 3.6 哈希编码
  • 3.7 Zipmap编码
  • 3.8 邮编编码
  • 3.9 intset 编码
  • 3.10 ziplist编码的有序集合
  • 3.11 Ziplist编码的Hashmap
  • 3.12 Quicklist编码


一、Redis RDB文件

Redis的RDB文件是内存存储的二进制表示。这个二进制文件足以完全恢复Redis的状态。

RDB文件格式针对快速读写进行了优化。在可能的情况下,使用LZF压缩来减小文件大小。一般情况下,对象都以它们的长度为前缀,因此在读取对象之前,您可以确切地知道要分配多少内存。

针对快速读写进行优化,意味着磁盘上的格式应该尽可能接近内存中的表示形式。这是RDB文件采用的方法。因此,如果不了解Redis在内存中的数据结构表示,就无法解析RDB文件。

二、解析RDB的高级算法

从一个较高的维度看,RDB文件的结构如下:

----------------------------#
52 45 44 49 53              # Magic String "REDIS"
30 30 30 33                 # RDB Version Number as ASCII string. "0003" = 3
----------------------------
FA                          # Auxiliary field
$string-encoded-key         # May contain arbitrary metadata
$string-encoded-value       # such as Redis version, creation time, used memory, ...
----------------------------
FE 00                       # Indicates database selector. db number = 00
FB                          # Indicates a resizedb field
$length-encoded-int         # Size of the corresponding hash table
$length-encoded-int         # Size of the corresponding expire hash table
----------------------------# Key-Value pair starts
FD $unsigned-int            # "expiry time in seconds", followed by 4 byte unsigned int
$value-type                 # 1 byte flag indicating the type of value
$string-encoded-key         # The key, encoded as a redis string
$encoded-value              # The value, encoding depends on $value-type
----------------------------
FC $unsigned long           # "expiry time in ms", followed by 8 byte unsigned long
$value-type                 # 1 byte flag indicating the type of value
$string-encoded-key         # The key, encoded as a redis string
$encoded-value              # The value, encoding depends on $value-type
----------------------------
$value-type                 # key-value pair without expiry
$string-encoded-key
$encoded-value
----------------------------
FE $length-encoding         # Previous db ends, next db starts.
----------------------------
...                         # Additional key-value pairs, databases, ...

FF                          ## End of RDB file indicator
8-byte-checksum             ## CRC64 checksum of the entire file.

2.1 Magic Number

文件以“REDIS”这个神奇的字符串开始。这是一个快速的完整性检查,以了解我们正在处理的是一个redis rdb文件。

52 45 44 49 53 # “REDIS”

2.2 RDB 版本号

接下来的4个字节存储rdb格式的版本号。这4个字节被解释为ASCII字符,然后使用字符串到整数的转换转换为整数。

30 30 30 33 # “0003” => Version 3

2.3 操作码

初始标头之后的每个部分由一个特殊的操作码引入。可用的操作码为:

字节

名称

描述

0xFF

EOF

RDB文件的结尾

0xFE

SELECTDB

数据库选择器

0xFD

EXPIRETIME

到期时间(以秒为单位)

0xFC

EXPIRETIMEMS

到期时间(以毫秒为单位)

0xFB

RESIZEDB

主键空间的哈希表大小和到期时间

0xFA

AUX

辅助字段。任意键值设置

2.3.1 数据库选择器

Redis实例可以具有多个数据库。

一个字节0xFE标记数据库选择器的开始。在此字节之后,可变长度字段指示数据库编号。请参阅“ 长度编码 ”部分,以了解如何读取此数据库编号。

2.3.2 Resizeb信息

此操作代码在RDB版本7中引入。

它对两个值进行编码,以通过避免其他大小调整和重新散列来加快RDB加载。操作码后跟两个长度编码的整数,指示:

  • 数据库哈希表大小
  • 过期哈希表大小

2.3.3 辅助字段

此操作代码在RDB版本7中引入。

操作码后跟两个Redis字符串,分别代表设置的键和值。解析器应忽略未知字段。

当前实现了以下设置:

  • redis-ver:编写RDB的Redis版本
  • redis-bits:编写RDB的系统的位体系结构,可以是32或64
  • ctime:RDB的创建时间
  • used-mem:编写RDB的实例的已用内存

2.3.4 键值对

在数据库选择器之后,该文件包含一系列键值对。

每个键值对都有4个部分:

  • key到期时间戳。这是可选的。
  • 1个字节的标志,指示值的类型。
  • key,编码为Redis字符串。请参阅字符串编码。
  • value,根据值类型进行编码的值。请参阅值编码。
key 到期时间戳

该部分以一个字节标志开始。该标志是:

  • 0xFD:以秒为单位指定以下过期值。以下4个字节将Unix时间戳表示为无符号整数。
  • 0xFC:指定以下过期值(以毫秒为单位)。以下8个字节将Unix时间戳表示为无符号长。

在导入过程中,必须丢弃已过期的密钥。

值类型

一个字节的标志指示用于保存值的编码。

  • 0 = 字符串编码
  • 1 = list编码
  • 2 = set编码
  • 3 = sort set编码
  • 4 = hash编码
  • 9 = Zipmap编码
  • 10 = Ziplist编码
  • 11 = Intset编码
  • 12 = 有序集合的Ziplist编码
  • 13 = Hashmap的Ziplist编码(在RDB版本4中引入)
  • 14 = 快速列表的编码(在RDB版本7中引入)

key是以Redis字符串进行编码的。请参阅“ 字符串编码 ”部分,以了解密钥的编码方式。

值是根据先前读取的“ 值类型”进行解析的。

2.4 CRC64校验码

从RDB版本5开始,将8字节CRC64校验和添加到文件末尾。可以通过redis.conf中的参数禁用此校验和。禁用校验和时,此字段将为零。

三、编码方式

3.1 Length Encoding 长度编码

长度编码用于存储流中下一个对象的长度。长度编码是一种可变字节编码,旨在使用尽可能少的字节。

长度编码的工作方式如下:从流中读取一个字节,比较两个最高有效位:

Bits

如何解析

00

接下来的6位代表长度

01

再读取一个字节。组合的14位代表长度

10

丢弃剩余的6位。流中接下来的4个字节表示长度

11

下一个对象以特殊格式编码。其余6位指示格式。可用于存储数字或字符串,请参见字符串编码

作为这种编码的结果:

  • 最多63个数字(包括63个数字)可以存储在1个字节中
  • 最多包括16383的数字可以存储在2个字节中
  • 最多232-1的数字可以存储在4个字节中

3.2 字符串编码

Redis字符串是二进制安全的——这意味着您可以在其中存储任何内容。它们没有任何特殊的字符串结尾标记。最好将Redis字符串视为字节数组。

Redis中有三种类型的字符串:

  • 长度前缀字符串
  • 8、16或32位整数
  • LZF压缩字符串

3.2.1 长度前缀字符串

长度前缀字符串非常简单。字符串的长度(以字节为单位)首先使用Length Encoding进行编码。此后,将存储字符串的原始字节。

3.2.2 整数作为字符串

首先,阅读“ 长度编码”部分,特别是前两位为11的部分。在这种情况下,将读取剩余的6位。

如果这6位的值是:

  • 0 表示跟随一个8位整数
  • 1 表示跟随一个16位整数
  • 2 表示后跟32位整数

3.2.3 压缩字符串

首先,阅读“ 长度编码”部分,特别是前两位为的部分11。在这种情况下,将读取剩余的6位。如果这6位的值为3,则表示紧随其后的是压缩字符串。

压缩字符串按如下方式进行读取:

  • 使用长度编码从流中读取压缩的长度 clen
  • 使用长度编码从流中读取未压缩的长度
  • 从流中读取下一个 clen 长度的压缩字节
  • 最后,使用LZF算法解压这些字节

3.3 列表编码

Redis列表表示为字符串序列。

  • 首先,size使用Length Encoding从流中读取列表的大小
  • 接下来,size使用String Encoding从流中读取字符串
  • 然后使用这些字符串重新构建列表

3.4 集合编码

集合编码与列表编码完全相同。

3.5 有序集合编码

  • 首先,使用长度编码从流中读取有序集合的数量大小size
  • 接下来,从流中读取 size 对 值及其分数。
  • 该值是字符串编码
  • 下一个字节指定分数编码的长度(无符号整数)。该字节具有3个特殊含义:
  • 253:不是数字。不读取其他字节
  • 254:正无穷大。不读取其他字节
  • 255:负无穷大。不读取其他字节
  • 从流中读取size那么多的字节,将分数表示为ASCII编码的浮点数。使用字符串到浮点转换来获取实际的双精度值。

注意:根据值的具体大小,您可能会丢失一些精度。Redis将分数保存为两倍。

注意:不能保证该集合已经排序。

例:

04
01 63 12 34 2E 30 31 39 39 39 39 39 39 39 39 39 39 39 39 39 36
01 64 FE
01 61 12 33 2E 31 38 39 39 39 39 39 39 39 39 39 39 39 39 39 39
01 65 FF
  • 首先,读取有序集合的大小:04 = 4(十进制)
  • 接下来,读取第一个数字,字符串编码。它的长度为 01 = 1(十进制)。读取一个字节:63 = c(ASCII)。
  • 然后读取下一个字节:12 = 18(十进制)。这是ASCII编码分数的长度。
  • 读取18个字节作为ASCII值:4.0199999999999996。如有必要,可以将其解析为双精度值。
  • 读取一个字节:01 = 1,下一个成员的长度。读取该字节:64 = d(ASCII)
  • 再读一个字节:FE = 254(十进制)。这意味着分数是正无穷大。
  • 读取下一个字节:01。同样,下一个成员的长度。读取该字节:61 = a。
  • 读取下一个字节:12 = 18(十进制)。读取接下来的18个字节,并将其解释为ASCII:""3.1899999999999999
  • 读取一个字节:01。同样,下一个成员的长度。读取该字节:65= e。
  • 再读一个字节:FF= 255(十进制)。这意味着分数是负无穷大。
    最终的排序集将是:
{ "e" => "-inf", "a" => "3.189999999999999", "c" => "4.0199999999999996", "d" => "+inf" }

3.6 哈希编码

  • 首先,使用长度编码从流中读取哈希的 size
  • 接下来,使用字符串编码从流中读取下一个 2 * size 的字符串(键和值是交替的字符串)

例:

2 us washington india delhi

表示:

{“us” => “washington”, “india” => “delhi”}

3.7 Zipmap编码

Zipmap是已序列化为字符串的哈希图。本质上,键值对是顺序存储的。在此结构中查找键的复杂度是O(N)。当键值对的数量很少时,使用此结构代替字典。

要解析zipmap,首先要使用字符串编码从流中读取一个字符串。该字符串的内容表示Zipmap编码。

一个字符串中的zipmap的结构如下:

“foo”“bar”“hello”“world”

  • zmlen:1个字节,用于保存Zipmap编码的大小。如果该值大于或等于254,则不使用值。您将必须迭代整个zip映射以找到长度。
  • len:接下来字符串的长度,可以是键或值。该长度以1字节或5字节存储(是的,它与上述的长度编码不同)。如果第一个字节在0到252之间,则这是zipmap的长度。如果第一个字节是253,则接下来的4个字节(无符号整数)表示zipmap的长度。254和255表示此字段是无效的。
  • free:始终为1个字节,表示该值之后的可用字节数。例如,如果key的值为“ America”,并且将其更新为“ USA”,则将有4个可用字节。
  • zmend:总是为255。表示zipmap的结尾。

例:

18 02 06 4D 4B 44 31 47 36 01 00 32 05 59 4E 4E 58 4b 04 00 46 37 54 49 FF …

  • 首先使用字符串对其进行解码。您会注意到0x18(用十进制表示为224)是字符串的长度。因此,我们将读取接下来的24个字节,即读到FF
  • 现在,我们从02 06…使用Zipmap编码开始解析字符串
  • 02 是hashmap中的条目数。
  • 06 是下一个字符串的长度。由于小于254,因此我们不必读取任何其他字节
  • 我们读取接下来的6个字节,即4d 4b 44 31 47 36得到key“ MKD1G6”
  • 01 是下一个字符串的长度,即为value
  • 00 是可用字节数
  • 我们读取下一个1字节,即0x32。因此,我们获得了value"2"
  • 在这种情况下,可用字节为0,因此我们不会跳过任何内容
  • 05 是下一个字符串的长度,也就是key的长度
  • 我们读取接下来的5个字节59 4e 4e 58 4b,以获取key"YNNXK"
  • 04 是下一个字符串的长度,它是一个值
  • 00 是值后的空闲字节数
  • 我们读取接下来的4个字节,即46 37 54 49获取值"F7TI"
  • 最后,我们遇到FF,它指示zip map的结尾
  • 因此,此zip map表示哈希 {“MKD1G6” => “2”, “YNNXK” => “F7TI”}

3.8 邮编编码

Ziplist是已序列化为字符串的列表。本质上,列表元素的标志和偏移量一起顺序存储,以实现在两个方向上高效遍历列表。

要解析ziplist,首先使用字符串编码从流中读取一个字符串。用此字符串的内容表示ziplist。

一个字符串中的ziplist的结构如下:


  • zlbytes:4字节的无符号整数,表示ziplist的总大小(以字节为单位)。4个字节采用小端格式——最低有效位在前
  • zltail:4字节无符号整数,采用小端格式。它代表了ziplist中尾部(即最后一个)条目的偏移量
  • zllen:这是2字节无符号整数,采用小端格式。它代表此ziplist中的条目数
  • entry:条目代表ziplist中的元素。详情如下
  • zlend:总是255。它代表ziplist的结尾。

ziplist中的每个 entry 都具有以下格式:


  • length-prev-entry:存储前一个条目的长度;如果是第一个条目,则存储0。这样可以轻松地反向浏览列表。该长度以1个字节或5个字节存储。如果第一个字节小于或等于253,则将其视为长度。如果第一个字节为254,则接下来的4个字节用于存储长度。4个字节作为无符号整数读取。
  • special-flag:此标志指示条目是字符串还是整数。它还指示字符串的长度或整数的大小。该标志的各种编码如下所示:

字节数

长度

含义

00pppppp

1个字节

长度小于或等于63个字节(6位)的字符串值

01pppppp(qqqqqqqq)

2字节

长度小于或等于16383字节(14位)的字符串值

10______<4 byte>

5字节

接下来的4个字节包含一个无符号的int。长度大于或等于16384个字节的字符串值

1100____

3个字节

整数编码为16位带符号(2个字节)

1101____

5字节

整数编码为32位带符号(4个字节)

1110____

9字节

整数编码为64位带符号(8个字节)

1111____

4字节

整数编码为24位带符号(3个字节)

  • raw-byte:在特殊标志之后,将输入原始字节。字节数先前已确定为特殊标志的一部分。

例:

23 23 00 00 00 1E 00 00 00 04 00 00 E0 FF FF FF FF FF
FF FF 7F 0A D0 FF FF 00 00 06 C0 FC 3F 04 C0 3F 00 FF …

-首先使用字符串编码对其进行解码。23是字符串的长度(十进制为35),因此我们将读取接下来的35个字节,直到FF
-现在,我们从23 00 00 …使用Ziplist编码开始解析字符串

  • 前4个字节23 00 00 00代表此ziplist的总长度(以字节为单位)。请注意:这是小端格式
  • 接下来的4个字节1e 00 00 00表示到尾项的偏移量。1E= 30(十进制),这是一个基于0的偏移量。第0位= 23,第1位= 00,依此类推。因此,最后一个条目开始于04 c0 3f 00 …
  • 接下来的2个字节04 00表示此列表中的条目数,为16位大端整数。04 00 = 4以十进制表示。
  • 从现在开始,我们开始读入entry
  • 00代表上一个条目的长度。0表示这是第一个条目。
  • E0是特殊标志。由于它以位模式1110____开头,因此我们将接下来的8个字节读取为整数。这是列表的第一项。
  • 现在开始第二项
  • 0A是上一个条目的长度。0A= 10以十进制表示。10个字节= 1个字节用于上一个的长度+ 1个字节(特殊标志)+ 8个字节(整数)。
  • D0是特殊标志。由于它以位模式1101____开头,因此我们将接下来的4个字节读取为整数。这是列表的第二项
  • 现在开始第三项
  • 06是上一个条目的长度。6字节= 1字节用于上一个的长度+ 1个字节(特殊标志)+ 4个字节(整数)
  • C0是特殊标志。由于它以位模式1100____开头,因此我们将接下来的2个字节读取为整数。这是列表的第三项
  • 现在开始最后一个条目
  • 04 是上一个条目的长度
  • C0 表示2个字节的数字
  • 我们读取接下来的2个字节,这是我们的第四项
  • 最后,我们遇到FF,这表明我们已经使用了该ziplist中的所有元素。
  • 因此,此ziplist存储值 [0x7fffffffffffffff, 65535, 16380, 63]

3.9 intset 编码

intset是整数的二进制搜索树。二叉树以整数数组实现。当集合的所有元素都是整数时,将使用intset。intset最多支持64位整数。作为一种优化,如果整数可以用更少的字节表示,则整数数组将由16位或32位整数构成。当插入新元素时,会在必要时进行升级。

由于Intset是二叉搜索树,因此该集合中的数字将始终被排序。

一个Intset具有Set的外部接口。

要解析一个Intset,首先使用字符串编码从您的流中读取一个字符串。此字符串的内容表示Intset。

在一个字符串中,Intset具有非常简单的布局:


  • encoding:是一个32位无符号整数。它具有3个可能的值-2、4或8。它表示存储在内容中的每个整数的大小(以字节为单位)。是的,这很浪费-我们可以将相同的信息存储在2个bit中。
  • length-of-contents:是一个32位无符号整数,表示内容数组的长度
  • contents:是 length-of-contents 个字节的数组。它包含整数的二叉树

14 04 00 00 00 03 00 00 00 FC FF 00 00 FD FF 00 00 FE FF 00 00 …

  • 首先使用“字符串编码”对此进行解码。14是字符串的长度,因此我们将读取接下来的20个字节,直到00
  • 现在,我们开始解释从以下位置开始的字符串 04 00 00 …
  • 前4个字节04 00 00 00是编码。由于这等于4,我们知道我们正在处理32位整数
  • 接下来的4个字节03 00 00 00是内容的长度。因此,我们知道我们正在处理3个整数,每个整数4个字节长
  • 从现在开始,我们以4个字节为一组读取,并将其转换为无符号整数
  • 因此,我们的整数看起来像[0x0000FFFC, 0x0000FFFD, 0x0000FFFE]。请注意,整数采用小尾数格式,即最低有效位在前。

3.10 ziplist编码的有序集合

以ziplist编码的有序集合像上面描述的Ziplist一样被排序好。在有序集合中,每个元素后面都跟着其分数。

[‘Manchester City’, 1, ‘Manchester United’, 2, ‘Tottenham’, 3]

如您所见,分数紧随每个元素。

3.11 Ziplist编码的Hashmap

在这种编码下,哈希映射的键值对作为连续条目存储在ziplist中。

注意:这是在RDB版本4中引入的。它不兼容在早期版本中使用的zipmap编码。

{“us” => “washington”, “india” => “delhi”}

在ziplist中存储为:

[“us”, “washington”, “india”, “delhi”]

3.12 Quicklist编码

RDB版本7引入了列表编码的新变体:快速列表Quicklist。

快速列表是一个压缩列表的链接列表。快速列表将小型ziplist的存储效率与链接列表的可扩展性结合在一起,使我们能够创建任意长度的节省空间的列表。

要解析快速列表,首先使用字符串编码从流中读取一个字符串。此字符串的内容表示ziplist。

一个字符串中的快速列表的结构如下:

  • len:这是链表的节点数,使用长度编码表示
  • ziplist:一个字符串,用于包装ziplist,并使用Ziplist编码进行解析

需要从ziplist的所有元素构建一个完整的列表。

例:

01 1F 1F 00 00 00 17 00 00 00 02 00 00 0B 6F 6E 65 2D 65 6C 65 6D 65 6E 74 0D 05 65 6C 65 6D 32 FF

  • 首先读取第一个字节,以获取长度编码中的列表长度。在这种情况下为01,因此列表仅包含一个节点。
  • 以下是一个包含该节点所有元素的压缩列表。
  • 通过阅读Ziplist编码中指定的ziplist继续进行。
  • 读取Blob(字符串编码)
  • 对于此,读取到的长度为 0x1F,因此有31个字节,直到FF
  • 读取这31个字节
  • 读取ziplist的字节数,为32位无符号整数:1F 00 00 00= 31(十进制)。
  • 读取尾部偏移量为32位无符号整数:17 00 00 00= 23(十进制)。它是从零开始的,所以这指向0D 05 65 6C …
  • 以16位无符号整数的形式读取ziplist的长度:02 00= 2(十进制)。
    现在,阅读两个ziplist条目,如下所示:
  • 读取下一个字节:00,因为这是第一项,并且没有前任项。
  • 读取下一个字节:0B= 11(十进制)。由于这是从位模式开始的,因此将00其视为长度。
  • 从流中读取11个字节:6F 6E 65 2D 65 6C 65 6D 65 6E 74= “one-element”(ASCII)
  • 这是第一个要素。
  • 继续第二项。
  • 读取一个字节:0D= 13(十进制)。这是上一项的长度(11的长度+标志+ prev-length)
  • 读取下一个字节:05。这是以下字符串的大小。
  • 读取接下来的5个字节:65 6C 65 6D 32= elem2。这是第二个列表元素。
  • 读取下一个字节:FF。这标志着ziplist的结尾(因为我们读取了之前确定的2个元素)
  • 因此,快速列表存储列表[“one-element”, “elem2”]。