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操作。 在操作码之后的两个值是:
- 数据库的哈希表大小。
- 失效哈希表的大小。
辅助字段
这个操作码是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使用双精度存放该分值。 注意:不能确保读取到的组合是有序的。
示例:
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为:
{
"e" => "-inf",
"a" => "3.189999999999999",
"c" => "4.0199999999999996",
"d" => "+inf"
}
Hash编码
示例:
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
- Windows Server AppFabric Caching支持大数据量的配置
- Python函数之匿名函数
- 配置Windows 2008 R2 防火墙允许远程访问SQL Server 2008 R2
- ASP.NET BBcode
- JavaScript 常用算法
- SVG 使用
- 小程能走网店模式?如何做分销模式的小程序?
- IIS 6 下配置以 FastCGI 跑 PHP
- IIS 7.x Application Request Routing(ARR) 502错误的解决方法
- 移动前端头部标签(HTML5 meta)
- 这个行业在汽车圈最落后,正在被物联网和AI改变
- WordPress 网站基于REST API 开发“微信小程序”实战
- 摩拜共享汽车亮相 首批新能源电动汽车试运行
- 基于SQL Server 2008 Service Broker构建企业级消息系统
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 求求你不要在用!=null判空了
- Geant4--root和csv文件存储
- 爬虫模拟登录破解无原图滑动验证码
- Flutter基础widgets教程-TabBarView篇
- python + selenium 爬虫模拟登录破解无原图滑动验证码
- python 轻量级定时框架apscheduler,周中定时给自己发送邮件。
- python 舆情分析 nlp主题分析 (1) 待续
- Flutter基础widgets教程-Text篇
- python 舆情分析 nlp主题分析 (2)-结合snownlp与jieba库,提高分词与情感判断 待续
- No qualifying bean of type 'com.pjh.service.Imp.serviceImp' available和Exception in thread "main" jav
- python音频文件中pcm格式提取
- Spring系列之事务的控制 注解实现+xml实现+事务的隔离等级
- Leetcode刷题 237. 删除链表中的节点 两行代码实现
- python提取视频第一帧图片
- Leetcode刷题 206. 反转链表 递归迭代两种方法实现