Redis RDB file format.
介绍第7版的RDB
Redis RDB文件格式
Redis的RDB文件是对内存存储的一种表示。这个二进制文件足以完全恢复Redis当时的运行状态。
RDB文件格式针对快速读写进行了优化。LZF压缩被用于减小文件大小。
通常,对象的长度会作为该条记录的前缀,所以在读取对象前,你已经精确地知道了需要分配多少内存。
优化文件的快速读写,意味着数据在磁盘中的格式,尽可能的和内存中展示的一样。
这就是RDB文件采用的方法。
因此,你可以在不了解Redis内存数据结构的前提下,解析RDB文件。
解析RBD文件的高级算法
整体看,RDB的文件结构如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
----------------------------#
52 45 44 49 53 # 文件魔术 字符串"REDIS"
30 30 30 33 # RDB的版本号,使用ASCII字符串表示: "0003" = 3
----------------------------
FA # 辅助字段
$string-encoded-key # 可能包含任意元数据信息
$string-encoded-value # 例如Redis的版本、创建时间、已使用的内存等等...
----------------------------
FE 00 # 指明数据库. db = 00
FB # 指明resizedb属性
$length-encoded-int # 相应Hash表的大小
$length-encoded-int # 相应带失效时间的Hash表大小
----------------------------# 键值对统计
FD $unsigned-int # "秒级超时", 紧挨着4个字节组成的无符号整数
$value-type # 1字节指明数据的编码方式
$string-encoded-key # KEY-键,使用Redis字符串编码方式
$encoded-value # VALUE-值,使用$value-type指明的编码方式
----------------------------
FC $unsigned long # "毫秒级超时", 紧挨着8个字节组成的无符号长整数
$value-type # 1字节指明数据的编码方式
$string-encoded-key # KEY-键,使用Redis字符串编码方式
$encoded-value # VALUE-值,使用$value-type指明的编码方式
----------------------------
$value-type # 没有失效时间的KV键值对
$string-encoded-key
$encoded-value
----------------------------
FE $length-encoding # 上一个DB的信息结束,下一个DB的信息。
----------------------------
... # 其他键值对、DB信息...
FF ## RDB文件结束标识
8-byte-checksum ## 8字节的CRC64表示的文件校验和
|
文件魔数 Magic Number
RDB文件以字符串“REDIS”作为开始。
这是一个快速完整性校验,用于判断是否在处理一个RDB文件。
52 45 44 49 53 # “REDIS”
RDB版本号
接下来的4个字节存放了RDB格式的版本号。
这4个字节是ASCII码()的值,然后使用字符串转整型的方式,将之转换成一个整数。
30 30 30 33 # “0003” => Version 3
在ASCII码中’0’ = 十进制48 = 十六禁制30。
ASCII码表
十进制 | ASCII码 | 十进制 | ASCII码 | 十进制 | ASCII码 | 十进制 | ASCII码 | |
---|---|---|---|---|---|---|---|---|
0 | NUT | 32 | (space) | 64 | @ | 96 | 、 | |
1 | SOH | 33 | ! | 65 | A | 97 | a | |
2 | STX | 34 | “ | 66 | B | 98 | b | |
3 | ETX | 35 | # | 67 | C | 99 | c | |
4 | EOT | 36 | $ | 68 | D | 100 | d | |
5 | ENQ | 37 | % | 69 | E | 101 | e | |
6 | ACK | 38 | & | 70 | F | 102 | f | |
7 | BEL | 39 | , | 71 | G | 103 | g | |
8 | BS | 40 | ( | 72 | H | 104 | h | |
9 | HT | 41 | ) | 73 | I | 105 | i | |
10 | LF | 42 | * | 74 | J | 106 | j | |
11 | VT | 43 | + | 75 | K | 107 | k | |
12 | FF | 44 | , | 76 | L | 108 | l | |
13 | CR | 45 | - | 77 | M | 109 | m | |
14 | SO | 46 | . | 78 | N | 110 | n | |
15 | SI | 47 | / | 79 | O | 111 | o | |
16 | DLE | 48 | 0 | 80 | P | 112 | p | |
17 | DCI | 49 | 1 | 81 | Q | 113 | q | |
18 | DC2 | 50 | 2 | 82 | R | 114 | r | |
19 | DC3 | 51 | 3 | 83 | S | 115 | s | |
20 | DC4 | 52 | 4 | 84 | T | 116 | t | |
21 | NAK | 53 | 5 | 85 | U | 117 | u | |
22 | SYN | 54 | 6 | 86 | V | 118 | v | |
23 | TB | 55 | 7 | 87 | W | 119 | w | |
24 | CAN | 56 | 8 | 88 | X | 120 | x | |
25 | EM | 57 | 9 | 89 | Y | 121 | y | |
26 | SUB | 58 | : | 90 | Z | 122 | z | |
27 | ESC | 59 | ; | 91 | [ | 123 | { | |
28 | FS | 60 | < | 92 | / | 124 | ||
29 | GS | 61 | = | 93 | ] | 125 | } | |
30 | RS | 62 | > | 94 | ^ | 126 | ` | |
31 | US | 63 | ? | 95 | _ | 127 | DEL |
操作码
在初始化的头部后,每个部分都由一个特殊操作码引入。可用的操作码如下:
Byte | 名称 | 描述 |
---|---|---|
0xFF | EOF | RDB文件的结尾 |
0xFE | SELECTDB | 数据库选择器 |
0xFD | EXPIRETIME | 秒级别的失效时间, 参照下面的Key失效时间戳介绍 |
0xFC | EXPIRETIMEMS | 毫秒级别的失效时间, 参照下面的Key失效时间戳介绍 |
0xFB | RESIZEDB | 主键值空间和失效键值的哈希表大小。参照下面的Resizedb信息介绍 |
0xFA | AUX | 辅助字段。任意键值对,参照下面的辅助字段介绍 |
数据库选择器
一个Redis实例可能有多个数据库。
一个字节0xFE用于标识数据库选择器部分的开始。
在该字节后,一个变长的字段表示数据库的索引值。
了解如何读取该数据库索引值,请参照长度编码。
Resizedb信息
这个操作码是RDB版本7引入的。
这部分包含两个编码后的值,用于加速RDB的加载,避免在加载过程中额外的调整hash空间(resize)和rehash操作。
在操作码之后的两个值是:
- 数据库的哈希表大小。
- 失效哈希表的大小。
辅助字段
这个操作码是RDB版本7引入的。
操作码后是两个Redis字符串,表示设置的KV键值对。
未知的字段会被解析器忽略。
目前实现的配置有:
- redis-ver:Redis的版本号
- redis-bits:输出该RDB文件的操作系统位架构,32或者64
- ctime:该RDB文件的创建时间
- used-mem:输出该RDB文件的Redis使用的内存大小
KV键值对
在数据库选择器信息后,这个文件包含了一系列的KV键值对序列。
每个KV键值对由4部分组成:
- Expiry Time:失效时间戳。这个字段是可选的。
- Value Type:一个字节的标识符,指明Value的类型。
- Key:使用Redis的String Encoding。参照String编码部分。
- Value:编码方式根据Value Type字段决定。参照编码部分。
失效时间戳
该部分由一个字节标识开始,该表示可以是以下两种之一:
- 0xFD:该标识表示,失效时间以秒为单位。接下来的4个字节组成一个无符号的整型,表示Unix时间戳。
- 0xFC:该标识表示,失效时间以毫秒为单位。接下来的8个字节组成一个无符号的长整型,表示Unix时间戳。
在导入过程中,如果key失效了,必须被忽略掉,即不加载。
值类型
一个字节来表示value使用的编码方式。
- 0 = String编码
- 1 = List编码
- 2 = Set编码
- 3 = SortedSet编码
- 4 = Hash编码
- 9 = ZipMap编码
- 10 = ZipList编码
- 11 = IntSet编码
- 12 = Sorted Set in ZipList编码
- 13 = Hashmap in ZipList编码(RDB版本4引入)
- 14 = List in QuickList编码 (RDB版本7引入)
键-Key
Key被当作一个Redis字符串进行编码。参照String编码部分,了解key是如何编码的。
值-Value
值的编码方式根据Value Type字段决定。参照编码部分。
编码
长度编码
长度编码用于存放,在流中下一个对象的长度。
长度编码是一个变长的字节编码,目的是尽可能的使用少量的字节。
长度编码的工作原理:从流中读取一个字节,比较两个最高有效比特(bit)位:
比特 | 如何解析 |
---|---|
00 | 接下来的6个bit表示长度 |
01 | 接下来的6个bit,加上再读取一个字节(即8bit),组成的14 bit表示长度。 |
10 | 忽略该字节剩下的6个bit。再从数据流中读取4个字节,表示长度。 |
11 | 接下来读取的对象使用了特殊编码。该字节剩余的6个bit表示编码格式。可能存放整数型或者字符串,参照String编码。 |
结论:
- 整数0-63(2^6-1)可以存放在1个字节中。
- 整数64-16383(2^14-1)可以存放在2个字节中。
- 整数16383-(2^32-1)可以存放在4个字节中。
String编码
Redis字符串是二进制安全-这意味着你可以存放任意数据。
字符串没有任何的特殊字符串作为结尾标记。
最好将Redis的字符串看作一个字节数组。
Redis有三种String类型:
- 长度作为前缀的字符串(Length Prefixed String)。
- 一个8bit、16bit或者64bit所表示的整数(Integer As String)。
- 使用LZF压缩的字符串(Compressed Strings)。
Length Prefixed String
长度作为前缀的字符串,格式非常简单,首先,字符串的字节长度使用长度编码
之后存放的就是该字符串的原始字节数组。
Integer As String
首先按照长度编码读取,开始的两个bit是11(二进制)。
在这种情况下,剩下的6个字节值的如果情况:
- 0,表示接下来是一个8bit的整数。
- 1,表示接下来是一个16bit的整数。
- 2,表示接下来是一个32bit的整数。
Compressed Strings
首先阅读长度编码章节,特别是开始的两个bit是11(二进制)的时候。
在这种情况下,读取剩下的6个bit值。如果这6bit的值是3,则表示接下来是一个压缩的字符串。
压缩字符串的读取方式如下:
List编码
一个Redis的List使用一系列字符串表示。
Set编码
set的编码方式和list一模一样。
SortedSet编码
- 首先,使用长度编码从数据流中读取Sorted Set的大小size。
- 然后,从数据流中读取size个值(value)、分数(score)组。
- 值使用String编码。
- 下一个byte执行压缩后分数(score)的长度(一个无符号整数)。这个byte有3个值具备特殊含义。
- 253:不是一个整数,不需要读取额外的byte。
- 254:正无穷大,不需要读取额外的byte。
- 255:负无穷下,不需要读取额外的byte。
- 从流中读取相应数量的byte,分值使用ASCII编码表示的float,使用String转换float的方式获取相应的浮点值。
注意:由于分值的精度可能失真。Redis使用双精度存放该分值。
注意:不能确保读取到的组合是有序的。
示例:
1
2
3
4
5
|
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
|
- 首先读取sorted set的大小:04 = 4 (十进制)
- 然后,读取第一个成员,使用String编码。它的长度01 = 1 (十进制)。读取一个byte:63 = c (十进制)
- 然后读取下一个byte:12 = 18 (十进制)。这是接下来score使用ASCII编码的字符串长度。
- 读取18个bytes,ASCII值:4.0199999999999996。如果需要,可以将其转换为double值。
- 读取一个byte:01 = 1, 即下一个成员的长度。读取一个byte:64 = d(ASCII)。
- 读取一个byte:FE = 254(十进制)。这表示该分值为正无穷大。
- 读取一个byte:01 = 1, 即下一个成员的长度。读取一个byte:61 = a(ASCII)。
- 读取一个byte:12 = 18 (十进制)。读取18个byte,将它们转换成ASCII码:3.1899999999999999。
- 读取一个byte:01 = 1, 即下一个成员的长度。读取一个byte:65 = e(ASCII)。
- 读取一个byte:FF = 255(十进制)。这表示该分值为负无穷小。
最后sorted set为:
1
2
3
4
5
6
|
{
"e" => "-inf",
"a" => "3.189999999999999",
"c" => "4.0199999999999996",
"d" => "+inf"
}
|
Hash编码
示例:
1
|
2 us washington india delhi
|
表示:
1
2
3
4
|
{
"us" => "washington",
"india" => "delhi"
}
|
ZipMap编码
一个ZipMap就是一个HashMap被序列化成的一个字符串。
实质上,键值对是被顺序存储的。
在该结构中查找一个key的算法时间复杂度是O(N)。
当键值对的数量较少时,使用该结构而不是字典结构。
解析一个ZipMap,首先使用String编码从流中读取一个字符串。这个字符串就表示该ZipMap。
一个ZipMap使用字符串的表达形式如下:
1
|
<zmlen><len>"foo"<len><free>"bar"<len>"hello"<len><free>"world"<zmend>
|
- zmlen:一个byte,表示该ZipMap的大小。如果该值大于或者等于254,则表示该值不可用。此时需要迭代整个ZipMap,算出该ZipMap的大小。
- len:下一个字符串的长度,该字符串可能是key或者是value。这个字符串的长度可能被1个byte或者5个byte表示(和上面长度编码不同)。如果第一个byte的值是0到252,那么这就是该ZipMap的长度,如果该byte值是253,则接下来的4个byte组成一个无符号整数,表示ZipMap的长度。对于该值254、255是非法的。
- free:这个字段总占用1个byte,并指明了该值后空闲byte的数量。例如,如果一个key的value是“America”,并且更新为“USA”,就会空闲4个byte。
- zend:该值总是255,表示ZipMap的结尾。
示例:
1
|
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 ..
|
- 首先使用String编码方式,反解码这个字符串。你会发现该字符串的长度是0x18(十进制值是24)。实际上,我们需要读取接下来的24个byte,即一直到FF。
- 现在,我们使用ZipMap编码解析该字符串02 06 …。
- 02是HashMap的条目数。
- 06是下一个字符串的长度,由于这个值小于254,我们不必读取额外的字节。
- 我们读取接下来的6个byte,即4d 4b 44 31 47 36,得到key:MKD1G6。
- 01是接下来字符串的长度,即value的长度。
- 00表示空闲字符串的个数,即0。
- 我们读取接下来的1个byte,即32,得到value:2。
- 在本条记录中,由于空闲字符串个数是0,所以不需要跳过任意byte。
- 05是下一个字符串的长度,即key。
- 我们读取接下来的5个byte,即59 4e 4e 58 4b,得到key:YNNXK。
- 04是接下来字符串的长度,即value的长度。
- 00表示空闲字符串的个数,即0。
- 我们读取接下来的4个byte,即46 37 54 49,得到value:F7TI。
这样,我们得到ZipMap如下:
1
2
3
4
|
{
"MKD1G6" => "2",
"YNNXK" => "F7TI"
}
|