# 解密 ziplist.
为什么叫解密 ziplist
呢?因为从 ziplist 中取到我们预期的值,真的和解密一样!烧脑,但是极其有趣!!
# 引题
在介绍 Redis
的数据类型 list
的时候,是我们第一次接触 ziplist
这一数据结构。
不知道是否还记得 ziplist
这种数据结构的特性。如果不记得也没有关系,今天我们来详细的看下这个数据结构。
# 重读 ziplist 数据结构
ziplist
是经过特殊编码的双向链接列表,旨在提高内存效率。 它同时存储字符串和整数值,其中整数被编码为实际整数,而不是一系列字符。它允许在 O(1)
时间在列表的任一侧进行推和弹出操作。
但是,由于每个操作都需要重新分配 ziplist
使用的内存,因此实际的复杂性与 ziplist
使用的内存量有关。
ziplist
的数据结构总览。如下图
我来依次解释下这 5
个部分.
-
<uint32_t zlbytes>
: 存储的ziplist
占用的字节数 (包含zlbytes
字段本身的4
个字节)。因此:无需遍历就能直接知道这个数据结构的大小。 -
<uint32_t zltail>
是列表中最后一个条目的偏移量。 这允许在列表的另一端进行弹出操作,而无需完全遍历。在任意一端进行pop
和push
操作的时间复杂度都是O(1)
. -
<uint16_t zllen>
是条目数。当条目数超过2 ^ 16-2
时,此值将设置为2 ^ 16-1
,这时,我们需要遍历整个列表以了解其包含多少项了。 -
<uint8_t zlend>
是代表ziplist
末尾的特殊条目。编码为等于255
的单个字节。 -
<entry>
: 往下看.
# Entry
ziplist
中的每个条目都以包含两个信息的元数据作为前缀。首先,存储前一个 entry
的长度,以便能够从后到前遍历列表。第二,提供 entry
编码。 它表示条目类型,整数或字符串,对于字符串,还表示字符串有效负载的长度。因此,能够像下面这样存储一个完整的 entry
.
有时, encoding
也可以代表 entry
本身,就像后面将要看到的小整数一样。在这种情况下, <entry-data>
部分丢失了,我们的 Entry
结构是这样的:
我们来具体看下 Entry
的编码方式.
# prevlen
<prevlen>
表示 前一个 entry
的长度。
-
如果 前一个
entry
的长度小于 254 个字节,那么这个Entry
只会消耗一个字节来表示该长度。一个无符号的8
位整数。 -
如果前一个
entry
长度大于或等于254
字节的时候,它将占用 5 个字节。第一个字节设置为254
(FE
), 以指示随后的值更大。 其他的四个字节将作为前一个 entry 的长度的值。
所以,我们就能知道了 下面这种编码方式.
<prevlen form 9 to 253> <encoding> <entry-data>
或者
<prevlen 4 bytes unsigned little endian prevlen> <encoding> <entry-data>
# encoding
entry
中的 encoding
字段取决于 entry-data
的内容。
-
如果
entry-data
是字符串的时候,encoding
第一个字节的前两位用于存储字符串长度的编码类型。然后是字符串的实际长度。 -
如果
entry
是整数时,前2
位都是1
,。 接下来的两位用于指定在此标头之后将存储哪种类型.
由上面的两点,就足以确定 entry-data
的类型.
不同的类型和编码的概述如下:
# 字符串类型
-
a
.| 00pppppp |
: 含义是占用1
个字节来表示长度小于或等于63
个字节(6
位)的字符串值。pppppp
表示无符号的 6 位长度。 -
b
.|01pppppp|qqqqqqqq|
含义是用2
个字节来表示小于等于16383(2^14)
个字节的字符串的长度. (这里使用大端法计数) -
c
.|10000000|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt|
含义是 使用 5 个字节来表示大于等于 16384 个字节长度的字符串,只有 4 个字节表示其最大长度为2^32-1
. 低位的 6 位被设置成0
.(这里使用大端法计数)
# 整数类型
d
.|11000000|
: 含义是用3
个字节表示一个16
位的有符号的短整形数 (short
)。e
.|11010000|
: 含义是用5
个字节 表示一个32
位的有符合整数型 (int
).f
.|11100000|
: 含义是用9
个字节,表示一个64
位的有符合长整数 (long
)g
.|11110000|
: 含义是用4
个字节表示24
位有符号整数 (只占用 3 个字节)。PS: 有什么作用呢?前面说的小整形数。节约空间。即如图h
.|11111110|
: 含义是用2
个字节表示一个8
位的有符号小整形数。1
个字节。i
.|1111xxxx|
:
除去上面列举的几类编码标识,还有|1111xxxx|
的类别。
(xxxx
在0000
和1101
之间) 立即4
位整数。0
到12
之间的无符号整数。编码值实际上是1
到13
,因为不能使用0000
和1111
,因此应从编码的4
位值中减去1
以获得正确的值。j
.|11111111|
- 表示ziplist
的特殊entry
这样,整个 Entry
的编码方式我们就搞明白了。
这些都是 Redis
中定义的规定, 所以我们记住就行了。如果让我们自己去设计一套编码方案的时候,我们就可以参考这种思路去设计。
# 那我们来举两个例子来试试身手吧。
# 例子 1: 25
# 编码方式
[0f 00 00 00] [0c 00 00 00] [02 00] [00 f3] [02 f6] [ff]
# 解析过程
# 例子 2: Hello World
这里我们还是接着上面例子来讲,我们再设置一个 字符串 Hello World
。
# 编码格式
[02] [0b] [48 65 6c 6c 6f 20 57 6f 72 6c 64]
# 解析过程
# 附录,ASCII 码表 (可展示字符)
以上内容,就是 Redis
的 ziplist
结构的内容了。
# 总结
本篇文章的内容主要是更加详细的分享了 ziplist
的这种数据结构的内部结构以及编码方式.
ziplist
由5
部分组成。存储了相关信息:<整个ziplist的长度><最后一个entry的偏移量><entry的总数><entries><表示结束的特殊entry>
- 一个
entry
由3
部分组成,<前一个entry的长度><编码方式><entry的内容>
- 其中 编码方式 是由 entry 的内容 决定的。有 10 条标准 (规定 / 协议)
- 引用了两个示例 (
25
和Hello World
),根据Redis
的方式来解码.
# 最后
期待您的关注,希望和你一起遇见更好的自己