Redis持久化文件RDB的格式解析

Redis RDB文件格式

Redis的RDB文件是对内存存储的一种表示。这个二进制文件足以完全恢复Redis当时的运行状态。 RDB文件格式针对快速读写进行了优化。LZF压缩被用于减小文件大小。 通常,对象的长度会作为该条记录的前缀,所以在读取对象前,你已经精确地知道了需要分配多少内存。 优化文件的快速读写,意味着数据在磁盘中的格式,尽可能的和内存中展示的一样。 这就是RDB文件采用的方法。 因此,你可以在不了解Redis内存数据结构的前提下,解析RDB文件。

解析RBD文件的高级算法

整体看,RDB的文件结构如下:

----------------------------#
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操作。 在操作码之后的两个值是:

  1. 数据库的哈希表大小。
  2. 失效哈希表的大小。

辅助字段

这个操作码是RDB版本7引入的。 操作码后是两个Redis字符串,表示设置的KV键值对。 未知的字段会被解析器忽略。

目前实现的配置有:

  • redis-ver:Redis的版本号
  • redis-bits:输出该RDB文件的操作系统位架构,32或者64
  • ctime:该RDB文件的创建时间
  • used-mem:输出该RDB文件的Redis使用的内存大小

KV键值对

在数据库选择器信息后,这个文件包含了一系列的KV键值对序列。 每个KV键值对由4部分组成:

  1. Expiry Time:失效时间戳。这个字段是可选的。
  2. Value Type:一个字节的标识符,指明Value的类型。
  3. Key:使用Redis的String Encoding。参照String编码部分。
  4. Value:编码方式根据Value Type字段决定。参照编码部分。

失效时间戳

该部分由一个字节标识开始,该表示可以是以下两种之一:

  • 0xFD:该标识表示,失效时间以秒为单位。接下来的4个字节组成一个无符号的整型,表示Unix时间戳。
  • 0xFC:该标识表示,失效时间以毫秒为单位。接下来的8个字节组成一个无符号的长整型,表示Unix时间戳。

在导入过程中,如果key失效了,必须被忽略掉,即不加载。

值类型

一个字节来表示value使用的编码方式。

键-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

长度作为前缀的字符串,格式非常简单,首先,字符串的字节长度使用长度编码 之后存放的就是该字符串的原始字节数组。

Integer As String

首先按照长度编码读取,开始的两个bit是11(二进制)。 在这种情况下,剩下的6个字节值的如果情况:

  • 0,表示接下来是一个8bit的整数。
  • 1,表示接下来是一个16bit的整数。
  • 2,表示接下来是一个32bit的整数。
Compressed Strings

首先阅读长度编码章节,特别是开始的两个bit是11(二进制)的时候。 在这种情况下,读取剩下的6个bit值。如果这6bit的值是3,则表示接下来是一个压缩的字符串。

压缩字符串的读取方式如下:

  • 从数据流中按照长度编码读取压缩字符串的长度clen
  • 从数据流中按照长度编码读取未压缩字符串的长度。
  • 从数据流中读取接下来clen长的byte。
  • 最后,这些byte使用LZF算法解压缩。

List编码

一个Redis的List使用一系列字符串表示。

  • 首先,列表的大小size,使用长度编码从数据流中读取。
  • 接着,size个字符串使用String编码方式读取。
  • 然后这个列表使用这些字符串重新创建。

Set编码

set的编码方式和list一模一样。

SortedSet编码

  1. 首先,使用长度编码从数据流中读取Sorted Set的大小size
  2. 然后,从数据流中读取size个值(value)、分数(score)组。
  3. 值使用String编码
  4. 下一个byte执行压缩后分数(score)的长度(一个无符号整数)。这个byte有3个值具备特殊含义。
    • 253:不是一个整数,不需要读取额外的byte。
    • 254:正无穷大,不需要读取额外的byte。
    • 255:负无穷下,不需要读取额外的byte。
  5. 从流中读取相应数量的byte,分值使用ASCII编码表示的float,使用String转换float的方式获取相应的浮点值。

注意:由于分值的精度可能失真。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
  1. 首先读取sorted set的大小:04 = 4 (十进制)
  2. 然后,读取第一个成员,使用String编码。它的长度01 = 1 (十进制)。读取一个byte:63 = c (十进制)
  3. 然后读取下一个byte:12 = 18 (十进制)。这是接下来score使用ASCII编码的字符串长度。
  4. 读取18个bytes,ASCII值:4.0199999999999996。如果需要,可以将其转换为double值。
  5. 读取一个byte:01 = 1, 即下一个成员的长度。读取一个byte:64 = d(ASCII)。
  6. 读取一个byte:FE = 254(十进制)。这表示该分值为正无穷大。
  7. 读取一个byte:01 = 1, 即下一个成员的长度。读取一个byte:61 = a(ASCII)。
  8. 读取一个byte:12 = 18 (十进制)。读取18个byte,将它们转换成ASCII码:3.1899999999999999
  9. 读取一个byte:01 = 1, 即下一个成员的长度。读取一个byte:65 = e(ASCII)。
  10. 读取一个byte:FF = 255(十进制)。这表示该分值为负无穷小。

最后sorted set为:

{
  "e" => "-inf", 
  "a" => "3.189999999999999", 
  "c" => "4.0199999999999996", 
  "d" => "+inf" 
}

Hash编码

  1. 首先,使用长度编码从数据流中读取Hash的大小size
  2. 然后从流中使用String编码读取2 * size个字符串。

示例:

2 us washington india delhi

表示:

{
  "us" => "washington", 
  "india" => "delhi"
}

ZipMap编码

一个ZipMap就是一个HashMap被序列化成的一个字符串。 实质上,键值对是被顺序存储的。 在该结构中查找一个key的算法时间复杂度是O(N)。 当键值对的数量较少时,使用该结构而不是字典结构。 解析一个ZipMap,首先使用String编码从流中读取一个字符串。这个字符串就表示该ZipMap。

一个ZipMap使用字符串的表达形式如下:

<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的结尾。

示例:

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如下:

{
  "MKD1G6" => "2", 
  "YNNXK" => "F7TI"
}

我的博客即将搬运同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=1tl6lbu6kcce

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏轮子工厂

5. 很“迷”的字符与字符串

最近一直在为自己的浏览量而担忧啦,都快被厂长大人约谈了……我真的有尽力在写稿子哦,所以也请各位老铁,如果觉得我的文章还不错就转发到朋友圈或者微信群之类的,让更多...

14920
来自专栏大内老A

ModelBinder——ASP.NET MVC Model绑定的核心

Model的绑定体现在从当前请求提取相应的数据绑定到目标Action方法的参数。通过前面的介绍我们知道Action方法的参数通过ParameterDescrip...

26390
来自专栏java一日一条

java面试小题系列(一)

11330
来自专栏丑胖侠

《Drools7.0.0.Final规则引擎教程》第4章 Function函数

Function函数 首先来看一下function函数的语法结构图: ? 函数是将语义代码放置在规则文件中的一种方式,就相当于java类中的方法一样。函数...

24680
来自专栏架构技术

C#重试公用类

12830
来自专栏老付的网络博客

详解.net中IL语言

中间语言,又称(IL语言)。充当Clr与.net 平台的中间语言,比如用C#编写程序,编译器首先是把C#代码转译成IL语言,最终由Clr解释执行,下面我们学习下...

20320
来自专栏Petrichor的专栏

python: json库 & .json文件读写

[1] python读写json文件 [2] 使用 python 读写中文json

26720
来自专栏Hongten

JavaScript

11020
来自专栏Brian

Python进阶教程(二)

概述 在上一篇博客中,我们介绍了Python进阶教程(一),还有一些新的技巧没有翻译完,我们下面来继续我们的翻译。 Intermediate Python 中译...

50980
来自专栏Kiba518

C#语法——元组类型

  我们现在使用的C#语法已经可以满足日常的开发需求,但C#语法还在进行版本的更新,在创造更多更优秀的语义来让我们使用。这里介绍一下C#5.0里的提供的语法——...

12830

扫码关注云+社区

领取腾讯云代金券