redis之压缩列表

redis之压缩列表

压缩列表

Ziplist 是由一系列特殊编码的内存块构成的列表,一个 ziplist 可以包含多个节点(entry),每个节点可以保存一个长度受限的字符数组(不以 \0 结尾的 char 数组)或者整数,包括:

• 字符数组

– 长度小于等于 63 (26 − 1)字节的字符数组

– 长度小于等于 16383 (214 − 1)字节的字符数组

长度小于等于 4294967295 (232 − 1)字节的字符数组

• 整数

– 4 位长,介于 0 至 12 之间的无符号整数

– 1 字节长,有符号整数

– 3 字节长,有符号整数

– int16_t 类型整数

– int32_t 类型整数

– int64_t 类型整数

因为 ziplist 节约内存的性质,它被哈希键、列表键和有序集合键作为初始化的底层实现来使用,更多信息请参考《哈希表》、《列表》和《有序集》章节。

本章先介绍 ziplist 的组成结构,以及 ziplist 节点的编码方式,然后介绍 ziplist 的添加操作和删除操作的执行过程,以及这两种操作可能引起的连锁更新现象,最后介绍 ziplist 的遍历方法和节点查找方式。

2.2.1 ziplist 的构成

下图展示了一个 ziplist 的典型分布结构:

area |<---- ziplist header ---->|<----------- entries ------------->|<-end->|

size 4 bytes 4 bytes 2 bytes ? ? ? ? 1 byte

±--------±-------±------±-------±-------±-------±-------±------+

component | zlbytes | zltail | zllen | entry1 | entry2 | … | entryN | zlend |

±--------±-------±------±-------±-------±-------±-------±------+

^ ^ ^

address | | | ZIPLIST_ENTRY_HEAD | ZIPLIST_ENTRY_END

| ZIPLIST_ENTRY_TAIL

图中各个域的作用如下:

长度**/**类型域的值
zlbytesuint32_t整个 ziplist 占用的内存字节数,对 ziplist 进行内存重分配,或 者计算末端时使用。
zltailuint32_t到达 ziplist 表尾节点的偏移量。通过这个偏移量,可以在不遍 历整个 ziplist 的前提下,弹出表尾节点。
zllenuint16_tziplist 中节点的数量。当这个值小于 UINT16_MAX(65535)时, 这个值就是 ziplist 中节点的数量;当这个值等于 UINT16_MAX 时,节点的数量需要遍历整个 ziplist 才能计算得出。
entryX?ziplist 所保存的节点,各个节点的长度根据内容而定。
zlenduint8_t255 的二进制值 1111 1111 (UINT8_MAX),用于标记 ziplist 的末端。

为了方便地取出 ziplist 的各个域以及一些指针地址,ziplist 模块定义了以下宏:

作用算法复杂度
ZIPLIST_BYTES(ziplist)取出 zlbytes 的值θ(1)
ZIPLIST_TAIL_OFFSET(ziplist)取出 zltail 的值θ(1)
ZIPLIST_LENGTH(ziplist)取出 zllen 的值θ(1)
ZIPLIST_HEADER_SIZE返回 ziplist header 部分的长度,总是固 定的 10 字节θ(1)
ZIPLIST_ENTRY_HEAD(ziplist)返回到达 ziplist 第一个节点(表头)的地 址θ(1)
ZIPLIST_ENTRY_TAIL(ziplist)返回到达 ziplist 最后一个节点(表尾)的 地址θ(1)
ZIPLIST_ENTRY_END(ziplist)返回 ziplist 的末端,也即是 zlend 之前 的地址θ(1)

因为 ziplist header 部分的长度总是固定的(4 字节 + 4 字节 + 2 字节),因此将指针移动到表头节点的复杂度为常数时间;除此之外,因为表尾节点的地址可以通过 zltail 计算得出,因此将指针移动到表尾节点的复杂度也为常数时间。

以下是用于操作 ziplist 的函数:

函数名作用算法复杂度
ziplistNew创建一个新的 ziplistθ(1)
ziplistResize重新调整 ziplist 的内存大小O(N )
ziplistPush将一个包含给定值的新节点推入 ziplist 的表头或 者表尾O(N 2)
zipEntry取出给定地址上的节点, 并将它的属性保存到 zlentry 结构然后返回θ(1)
ziplistInsert将一个包含给定值的新节点插入到给定地址O(N 2)
ziplistDelete删除给定地址上的节点O(N 2)
ziplistDeleteRange在给定索引上,连续进行多次删除O(N 2)
ziplistFind在 ziplist 中查找并返回包含给定值的节点O(N )
ziplistLen返回 ziplist 保存的节点数量O(N )
ziplistBlobLen以字节为单位,返回 ziplist 占用的内存大小θ(1)

因为 ziplist 由连续的内存块构成,在最坏情况下,当 ziplistPush 、ziplistDelete 这类对节点进行增加或删除的函数之后,程序需要执行一种称为连锁更新的动作来维持 ziplist 结构本身的性质,所以这些函数的最坏复杂度都为 O(N 2) 。不过,因为这种最坏情况出现的概率并不高,所以大可以放心使用 ziplist ,而不必太担心出现最坏情况。

2.2.2 节点的构成

一个 ziplist 可以包含多个节点,每个节点可以划分为以下几个部分:

area |<------------------- entry -------------------->|

±-----------------±---------±-------±--------+

component | pre_entry_length | encoding | length | content |

±-----------------±---------±-------±--------+

以下几个小节将分别对这个四个部分进行介绍。

pre_entry_length

pre_entry_length 记录了前一个节点的长度,通过这个值,可以进行指针计算,从而跳转到上一个节点。

area |<---- previous entry —>|<--------------- current entry ---------------->|

size 5 bytes 1 byte ? ? ?

±------------------------±----------------------------±-------±--------+

component | … | pre_entry_length | encoding | length | content |

| | | | | |

value | | 0000 0101 | ? | ? | ? |

±------------------------±----------------------------±-------±--------+

^ ^

address | |

p = e - 5 e

上图展示了如何通过一个节点向前跳转到另一个节点: 用指向当前节点的指针 e , 减去

pre_entry_length 的值(0000 0101 的十进制值,5),得出的结果就是指向前一个节点的地址 p 。

根据编码方式的不同,pre_entry_length 域可能占用 1 字节或者 5 字节:

• 1 字节:如果前一节点的长度小于 254 字节,那么只使用一个字节保存它的值。

• 5 字节:如果前一节点的长度大于等于 254 字节,那么将第 1 个字节的值设为 254 ,然后用接下来的 4 个字节保存实际长度。

作为例子,以下是一个长度为 1 字节的 pre_entry_length 域,域的值为 128 (二进制为 1000 0000 ):

area |<------------------- entry -------------------->|

size 1 byte ? ? ?

±-----------------±---------±-------±--------+

component | pre_entry_length | encoding | length | content |

| | | | | value | 1000 0000 | | | |

±-----------------±---------±-------±--------+

而以下则是一个长度为 5 字节的 pre_entry_length 域,域的第一个字节被设为 254 的二进制

1111 1110 ,而之后的四个字节则被设置为 10086 的二进制 10 0111 0110 0110 (多余的高

位用 0 补完):

area |<------------------------------ entry ---------------------------------->|

size 5 bytes ? ? ?

±------------------------------------------±---------±-------±--------+

component | pre_entry_length | encoding | length | content |

| | | | |

| 11111110 00000000000000000010011101100110 | ? | ? | ? |

±------------------------------------------±---------±-------±--------+

|<------->|<------------------------------->|

1 byte 4 bytes

encoding 和 length

encoding 和 length 两部分一起决定了 content 部分所保存的数据的类型(以及长度)。其中,encoding 域的长度为两个 bit ,它的值可以是 00 、01 、10 和 11 :

• 00 、01 和 10 表示 content 部分保存着字符数组。

• 11 表示 content 部分保存着整数。

以 00 、01 和 10 开头的字符数组的编码方式如下:

编码编码长度content 部分保存的值
00bbbbbb1byte长度小于等于 63 字节的 字符数组。
01bbbbbb xxxxxxxx2byte长度小于等于 16383 字 节的字符数组。
10 aaaaaaaa bbbbbbbb cccccccc dddddddd5byte长 度 小 4294967295 数组。于 等 于的 字 符

表格中的下划线 _ 表示留空,而变量 b 、x 等则代表实际的二进制数据。为了方便阅读,多个字节之间用空格隔开。

11 开头的整数编码如下:

编码编码长度content 部分保存的值
110000001 byteint16_t 类型的整数
110100001 byteint32_t 类型的整数
111000001 byteint64_t 类型的整数
111100001 byte24 bit 有符号整数
111111101 byte8 bit 有符号整数
1111xxxx1 byte4 bit 无符号整数,介于 0 至 12 之间

content

content 部分保存着节点的内容,它的类型和长度由 encoding 和 length 决定。以下是一个保存着字符数组 hello world 的节点的例子:

area |<---------------------- entry ----------------------->|

size ? 2 bit 6 bit 11 byte

±-----------------±---------±-------±--------------+

component | pre_entry_length | encoding | length | content |

| | | | | value | ? | 00 | 001011 | hello world |

±-----------------±---------±-------±--------------+

encoding 域的值 00 表示节点保存着一个长度小于等于 63 字节的字符数组,length 域给出了这个字符数组的准确长度——11 字节(的二进制 001011),content 则保存着字符数组值 hello world 本身(为了表示的简单,content 部分使用字符而不是二进制表示)。

以下是另一个节点,它保存着整数 10086 :

area |<---------------------- entry ----------------------->|

size ? 2 bit 6 bit 2 bytes

±-----------------±---------±-------±--------------+

component | pre_entry_length | encoding | length | content |

| | | | | value | ? | 11 | 000000 | 10086 |

±-----------------±---------±-------±--------------+

encoding 域的值 11 表示节点保存的是一个整数;而 length 域的值 000000 表示这个节点的值的类型为 int16_t ;最后,content 保存着整数值 10086 本身(为了表示的简单,content 部分用十进制而不是二进制表示)。

2.2.3 创建新 ziplist

函数 ziplistNew 用于创建一个新的空白 ziplist ,这个 ziplist 可以表示为下图:

area |<---- ziplist header ---->|<-- end -->|

size 4 bytes 4 bytes 2 bytes 1 byte

±--------±-------±------±----------+

component | zlbytes | zltail | zllen | zlend |

| | | | | value | 1011 | 1010 | 0 | 1111 1111 |

±--------±-------±------±----------+

^

| ZIPLIST_ENTRY_HEAD

&

address ZIPLIST_ENTRY_TAIL &

ZIPLIST_ENTRY_END

空白 ziplist 的表头、表尾和末端处于同一地址。

创建了 ziplist 之后,就可以往里面添加新节点了,根据新节点添加位置的不同,这个工作可以分为两类来进行:

\1. 将节点添加到 ziplist 末端:在这种情况下,新节点的后面没有任何节点。

\2. 将节点添加到某个/某些节点的前面:在这种情况下,新节点的后面有至少一个节点。 以下两个小节分别讨论这两种情况。

2.2.4 将节点添加到末端

将新节点添加到 ziplist 的末端需要执行以下三个步骤:

\1. 记录到达 ziplist 末端所需的偏移量(因为之后的内存重分配可能会改变 ziplist 的地址, 因此记录偏移量而不是保存指针)

\2. 根据新节点要保存的值,计算出编码这个值所需的空间大小,以及编码它前一个节点的 长度所需的空间大小,然后对 ziplist 进行内存重分配。

\3. 设置新节点的各项属性:pre_entry_length 、encoding 、length 和 content 。

\4. 更新 ziplist 的各项属性,比如记录空间占用的 zlbytes ,到达表尾节点的偏移量 zltail

,以及记录节点数量的 zllen 。

举个例子,假设现在要将一个新节点添加到只含有一个节点的 ziplist 上,程序首先要执行步骤

1 ,定位 ziplist 的末端:

area |<---- ziplist header ---->|<— entries -->|<-- end -->|

size 4 bytes 4 bytes 2 bytes 5 bytes 1 bytes

±--------±-------±------±---------------±----------+

component | zlbytes | zltail | zllen | entry 1 | zlend |

| | | | | | value | 10000 | 1010 | 1 | ? | 1111 1111 |

±--------±-------±------±---------------±----------+

^ ^

| |

address ZIPLIST_ENTRY_HEAD ZIPLIST_ENTRY_END

& ZIPLIST_ENTRY_TAIL

然后执行步骤 2 ,程序需要计算新节点所需的空间:

假设我们要添加到节点里的值为字符数组 hello world ,那么保存这个值共需要 12 字节的空间:

• 11 字节用于保存字符数组本身;

• 另外 1 字节中的 2 bit 用于保存类型编码 00 ,而其余 6 bit 则保存字符数组长度 11 的二进 制 001011 。

另外,节点还需要 1 字节,用于保存前一个节点的长度 5 (二进制 101 )。

合算起来,为了添加新节点,ziplist 总共需要多分配 13 字节空间。以下是分配完成之后,

ziplist 的样子:

area |<---- ziplist header ---->|<------------ entries ------------>|<-- end -->|

size 4 bytes 4 bytes 2 bytes 5 bytes 13 bytes 1 bytes

±--------±-------±------±---------------±-----------------±----------+

component | zlbytes | zltail | zllen | entry 1 | entry 2 | zlend |

| | | | | | | value | 10000 | 1010 | 1 | ? | pre_entry_length | 1111 1111 |

| | | | | ? | |

| | | | | | |

| | | | | encoding | |

| | | | | ? | |

| | | | | | |

| | | | | length | |

| | | | | ? | |

| | | | | | |

| | | | | content | |

| | | | | ? | |

| | | | | | |

±--------±-------±------±---------------±-----------------±----------+

^ ^

| |

address ZIPLIST_ENTRY_HEAD ZIPLIST_ENTRY_END

& ZIPLIST_ENTRY_TAIL

步骤三,更新新节点的各项属性(为了表示的简单,content 的内容使用字符而不是二进制来表示):

area |<---- ziplist header ---->|<------------ entries ------------>|<-- end -->|

size 4 bytes 4 bytes 2 bytes 5 bytes 13 bytes 1 bytes

±--------±-------±------±---------------±-----------------±----------+

component | zlbytes | zltail | zllen | entry 1 | entry 2 | zlend |

| | | | | | | value | 10000 | 1010 | 1 | ? | pre_entry_length | 1111 1111 |

| | | | | 101 | |

| | | | | | |

| | | | | encoding | |

| | | | | 00 | |

| | | | | | |

| | | | | length | |

| | | | | 001011 | |

| | | | | | |

| | | | | content | |

| | | | | hello world | |

| | | | | | |

±--------±-------±------±---------------±-----------------±----------+

^ ^

| |

address ZIPLIST_ENTRY_HEAD ZIPLIST_ENTRY_END

& ZIPLIST_ENTRY_TAIL

最后一步,更新 ziplist 的 zlbytes 、zltail 和 zllen 属性:

area |<---- ziplist header ---->|<------------ entries ------------>|<-- end -->|

size 4 bytes 4 bytes 2 bytes 5 bytes 13 bytes 1 bytes

±--------±-------±------±---------------±-----------------±----------+

component | zlbytes | zltail | zllen | entry 1 | entry 2 | zlend |

| | | | | | | value | 11101 | 1111 | 10 | ? | pre_entry_length | 1111 1111 |

| | | | | 101 | |

| | | | | | |

| | | | | encoding | |

| | | | | 00 | |

| | | | | | |

| | | | | length | |

| | | | | 001011 | |

| | | | | | |

| | | | | content | |

| | | | | hello world | |

| | | | | | |

±--------±-------±------±---------------±-----------------±----------+

^ ^ ^

| | |

address | ZIPLIST_ENTRY_TAIL ZIPLIST_ENTRY_END

| ZIPLIST_ENTRY_HEAD

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tJzaVliW-1610780976623)(E:\markdown\图爿\clip_image001.gif)]

到这一步,添加新节点到表尾的工作正式完成。

Note: 这里没有演示往空 ziplist 添加第一个节点的过程,因为这个过程和上面演示的添加第二个节点的过程类似;而且因为第一个节点的存在,添加第二个节点的过程可以更好地展示“将 节点添加到表尾”这一操作的一般性。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XZxG3WKq-1610780976629)(E:\markdown\图爿\clip_image002.gif)]

2.2.5 将节点添加到某个**/**某些节点的前面

比起将新节点添加到 ziplist 的末端,将一个新节点添加到某个/某些节点的前面要复杂得多, 因为这种操作除了将新节点添加到 ziplist 以外,还可能引起后续一系列节点的改变。

举个例子,假设我们要将一个新节点 new 添加到节点 prev 和 next 之间:

add new entry here

| V

±---------±---------±---------±---------±---------+

| | | | | |

| prev | next | next + 1 | next + 2 | … |

| | | | | |

±---------±---------±---------±---------±---------+

程序首先为新节点扩大 ziplist 的空间:

±---------±---------±---------±---------±---------±---------+

| | | | | | |

| prev | ??? | next | next + 1 | next + 2 | … |

| | | | | | |

±---------±---------±---------±---------±---------±---------+

|<-------->|

expand space

然后设置 new 节点的各项值——到目前为止,一切都和前面介绍的添加操作一样:

set value, property, length, etc.

| v

±---------±---------±---------±---------±---------±---------+

| | | | | | |

| prev | new | next | next + 1 | next + 2 | … |

| | | | | | |

±---------±---------±---------±---------±---------±---------+

现在,新的 new 节点取代原来的 prev 节点,成为了 next 节点的新前驱节点,不过,因为这时next 节点的 pre_entry_length 域编码的仍然是 prev 节点的长度,所以程序需要将 new 节点的长度编码进 next 节点的 pre_entry_length 域里,这里会出现三种可能:

\1. next 的 pre_entry_length 域的长度正好能够编码 new 的长度(都是 1 字节或者都是 5

字节)

\2. next 的 pre_entry_length 只有 1 字节长,但编码 new 的长度需要 5 字节

\3. next 的 pre_entry_length 有 5 字节长,但编码 new 的长度只需要 1 字节对于情况 1 和 3 ,程序直接更新 next 的 pre_entry_length 域。

如果是第二种情况,那么程序必须对 ziplist 进行内存重分配,从而扩展 next 的空间。然而, 因为 next 的空间长度改变了,所以程序又必须检查 next 的后继节点——next+1 ,看它的

pre_entry_length 能否编码 next 的新长度,如果不能的话,程序又需要继续对 next+1 进行扩容。。。

这就是说,在某个/某些节点的前面添加新节点之后,程序必须沿着路径一个个检查后续的节 点是否满足新长度的编码要求,直到遇到一个能满足要求的节点(如果有一个能满足,那么这个节点之后的其他节点也满足),或者到达 ziplist 的末端 zlend 为止,这种检查操作的复杂度为 O(N 2) 。

不过,因为只有在新添加节点的后面有连续多个长度接近 254 的节点时,这种连锁更新才会发生,所以可以普遍地认为,这种连锁更新发生的概率非常小,在一般情况下,将添加操作看成是 O(N ) 复杂度也是可以的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DhcSGCr7-1610780976630)(E:\markdown\图爿\clip_image003.gif)]

执行完这三种情况的其中一种后,程序更新

ziplist

的各项属性,至此,添加操作完成。

Note: 在第三种情况中,程序实际上是可以执行类似于情况二的动作的:它可以一个个地检查新节点之后的节点,尝试收缩它们的空间长度,不过 Redis 决定不这么做,因为在一些情况下,比如前面提到的,有连续多个长度接近 254 的节点时,可能会出现重复的扩展——收缩

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zRcvGy6j-1610780976631)(E:\markdown\图爿\clip_image004.gif)]

——

再扩展——再收缩的抖动(

flapping

)效果,这会让操作的性能变得非常差。

2.2.6 删除节点

删除节点和添加操作的步骤类似。

  1. 定位目标节点,并计算节点的空间长度 target-size :

target start here

| V

±---------±---------±---------±---------±---------±---------+

| | | | | | |

| prev | target | next | next + 1 | next + 2 | … |

| | | | | | |

±---------±---------±---------±---------±---------±---------+

|<-------->|

target-size

  1. 进行内存移位,覆盖 target 原本的数据,然后通过内存重分配,收缩多余空间:

target start here

| V

±---------±---------±---------±---------±---------+

| | | | | |

| prev | next | next + 1 | next + 2 | … |

| | | | | |

±---------±---------±---------±---------±---------+

| <------------------------------------------ memmove

  1. 检查 next 、next+1 等后续节点能否满足新前驱节点的编码。和添加操作一样,删除操作也可能会引起连锁更新。

2.2.7 遍历

可以对 ziplist 进行从前向后的遍历,或者从后先前的遍历。

当进行从前向后的遍历时,程序从指向节点 e1 的指针 p 开始,计算节点 e1 的长度(e1-size),然后将 p 加上 e1-size ,就将指针后移到了下一个节点 e2 。。。一直这样做下去,直到 p 遇到

ZIPLIST_ENTRY_END 为止,这样整个 ziplist 就遍历完了:

p + e1-size + e2-size

p + e1-size |

p | |

| | |

V V V

±---------±---------±---------±---------±---------±---------±---------+

| ZIPLIST | | | | | | ZIPLIST |

| ENTRY | e1 | e2 | e3 | e4 | … | ENTRY |

| HEAD | | | | | | END |

±---------±---------±---------±---------±---------±---------±---------+

|<-------->|<-------->|

e1-size e2-size

当进行从后往前遍历的时候,程序从指向节点 eN 的指针 p 出发,取出 eN 的 pre_entry_length 值,然后用 p 减去 pre_entry_length ,这就将指针移动到了前一个节点 eN-1 。。。一直这样做下去,直到 p 遇到 ZIPLIST_ENTRY_HEAD 为止,这样整个 ziplist 就遍历完了。

p - eN.pre_entry_length

|

| p

| |

V V

±---------±---------±---------±---------±---------±---------±---------+

| ZIPLIST | | | | | | ZIPLIST |

| ENTRY | e1 | e2 | … | eN-1 | eN | ENTRY |

| HEAD | | | | | | END |

±---------±---------±---------±---------±---------±---------±---------+

2.2.8 查找元素、根据值定位节点

这两个操作和遍历的原理基本相同,不再赘述。

2.2.9 小结

• ziplist 是由一系列特殊编码的内存块构成的列表,它可以保存字符数组或整数值,它还是哈希键、列表键和有序集合键的底层实现之一。

• ziplist 典型分布结构如下:

area |<---- ziplist header ---->|<----------- entries ------------->|<-end->|

size 4 bytes 4 bytes 2 bytes ? ? ? ? 1 byte

±--------±-------±------±-------±-------±-------±-------±------+

component | zlbytes | zltail | zllen | entry1 | entry2 | … | entryN | zlend |

±--------±-------±------±-------±-------±-------±-------±------+

^ ^ ^

address | | | ZIPLIST_ENTRY_HEAD | ZIPLIST_ENTRY_END

| ZIPLIST_ENTRY_TAIL

• ziplist 节点的分布结构如下:

area |<------------------- entry -------------------->|

±-----------------±---------±-------±--------+

component | pre_entry_length | encoding | length | content |

±-----------------±---------±-------±--------+

• 添加和删除 ziplist 节点有可能会引起连锁更新,因此,添加和删除操作的最坏复杂度为O(N 2) ,不过,因为连锁更新的出现概率并不高,所以一般可以将添加和删除操作的复杂度视为 O(N ) 。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值