【最完整系列】Redis-结构篇-压缩列表

什么是压缩列表

压缩列表 ziplist 在 redis 中的应用也很是普遍,它是咱们经常使用的 zset ,list 和 hash 结构的底层实现之一。当咱们的容器对象的元素个数小于必定条件时,redis 会使用 ziplist 的方式储存,来减小内存的使用。redis

> hset test_hash me sidfate
    (integer) 1
    > object encoding test_hash
    "ziplist"
复制代码

为何要在元素较少的时候使用 ziplist ?shell

由于 redis 中的集合容器中,不少状况都用到了链表的实现,元素和元素之间经过储存的关联指针有序的串联起来,可是这样的指针每每是 随机I/O,也就是指针地址是不连续的(分布不均匀)。而咱们的 ziplist 它自己是一块连续的内存块,因此它的读写是 顺序I/O,从底层的磁盘读写来讲,顺序I/O 的效率确定是高于 随机I/O 。你可能会问了,那为何不都用 顺序I/O 的 ziplist 代替 随机I/O 呢,由于 ziplist 是连续内存,当你元素数量多了,意味着当你建立和扩展的时候须要操做更多的内存,因此 ziplist 针对元素少的时候才能提高效率。数组

ziplist 如何减小内存使用的呢?布局

接下来让咱们从源码中一探究竟。编码

源码结构

题外话:每当你想要去探究一个项目的源码的时候,首先应该去看的就是它的注释,好的注释便是文档。同时也告诉咱们平时开始也要注意注释的编写。spa

首先从源码的注释中咱们能够了解一些基础信息:指针

ziplist 是通过特殊编码的双向列表结构,用来提升内存使用效率。它能够储存字符串或者整数值,其中整数值被编码成实际的整数,而不是字符串形式。它能够在 O(1) 时间内对列表的两端进行 push 和 pop 操做。可是,由于每一个操做都须要从新分配 ziplist 使用的内存,因此实际的复杂度与 ziplist 使用的内存大小有关。code

ziplist 结构的布局以下:cdn

<zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>
复制代码

属性 字节数 含义
zlbytes 4 压缩列表占用的内存字节数:在对压缩列表进行内存重分配, 或者计算 zlend 的位置时使用。
zltail 4 压缩列表表尾节点的偏移量:用来倒序遍历压缩列表。
zllen 2 记录了压缩列表包含的节点数量: 当这个属性的值小于 UINT16_MAX (65535)时, 这个属性的值就是压缩列表包含节点的数量; 当这个值等于 UINT16_MAX 时, 节点的真实数量须要遍历整个压缩列表才能计算得出。
entry[] 待定 节点数组,包含元素的具体信息
zlend 1 特殊值 0xFF (十进制 255 ),用于标记压缩列表的末端。

ziplist中的每一个节点 entry 的结构以下:对象

<prevlen> <encoding> <entry-data>
复制代码

redis 为了节约内存在 ziplist 的 entry 这个结构上有不少骚操做,让我来一一说明。

prevlen

prevlen 表示前一个元素的长度,以便可以从后向前遍历列表。它有一套特别的编码方式:若是这个长度小于254字节,那么它占用1个字节;当长度大于或等于254时,占用5个字节,第一个字节被设置为254 (0xFE),其他的4个字节采用前一个条目的长度做为值。prevlen 用 5 bytes表示时,不表明长度必定大于等于254,这是为了减小 realloc 和 memmove 提升效率。

为何临界值是 254 ?咱们来算一笔,一个字节最大能储存值为255,那临界值应该是255啊,别忘了咱们还有个 zlend,它的值是0xFF(255),为了不混淆,因此用254区分。

encoding

encoding 表示元素的编码,它取决于元素的内容。当元素是一个字符串时,编码的第一个字节的前2位将保存用于存储字符串长度的编码类型,而后是字符串的实际长度。当条目是整数时,前2位都设置为1。下面的2位用于指定在这个报头以后将存储哪一种类型的整数。对不一样类型和编码的概述以下。第一个字节老是足以肯定条目的类型。

  • |00pppppp| - 1 字节

    长度小于或等于63字节的字符串,63能够用6个字节表示,因此 pppppp 表示字符串的实际长度。

  • |01pppppp|qqqqqqqq| - 2 字节

    长度小于或等于16383字节(14 位)的字符串。

  • |10000000|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt| - 5 字节

    长度大于16383(14位)的字符串,后4个字节表明长度。

  • |11000000| - 3 字节

    11000000 + int16(2字节)。

  • |11010000| - 5 字节

    11010000 + int32(4字节)。

  • |11100000| - 9 bytes

    11010000 + int64(8字节)。

  • |11110000| - 4 bytes

    11110000 + 24位有符号整数(3字节)。

  • |11111110| - 2 bytes

    11110000 + int8(1字节)。

  • |1111xxxx|

    极小整数,xxxx 的范围只能是 (0001~1101),也就是1~13,可是由于0000、11十、1111都被占用了。读取到的 value 须要将 xxxx 减 1,也就是整数 0~12 就是最终的 value。

  • |11111111|

    表示 ziplist 的结束,也就是 zlend 的值 0xFF。

若是你以为看的混乱了,别慌,上面的不须要所有记住,下面我会用一个鲜活的栗子(官方例子)来总结下。如下是一个包含了字符串 “2” 和 “5” 的压缩列表:

[0f 00 00 00] [0c 00 00 00] [02 00] [00 f3] [02 f6] [ff]
      |             |          |       |       |     |
   zlbytes        zltail    zllen     "2"     "5"   end
复制代码

最前面的4个字节的表明着数字 0x0f = 15(zlbytes = 15),表示这个 ziplist 总共占用了15个字节。紧跟着的4个字节表明数字 0x0c = 12(zltail = 12),说明最后一个元素的偏移量是12,也就是 “5” 这个元素到 ziplist 开头的长度。接着是 zllen = 2,表明总共有2个元素。以后就是实际储存 “2” 和 “5” 的 entry。解读下 “2” 为何是 00 f3,00表示前一个元素长度为0,由于它是第一个元素,f3 是 0x11110011,也就是咱们的 1111xxxx encoding类型,3 - 1 = 2正好是咱们的 “2”,“5” 也同理。 最后是 ff 结尾表示结束。

有没有童鞋注意到官方例子总咱们储存的是字符串的 “2” 和 “5”,可是 redis 把它当成了整数来储存 ?这一点实际上是 redis 故意作的,不少地方都会作相似处理,目的么,仍是为了减小内存消耗。

最后咱们再看一个储存字符串的例子,咱们把上面的 “5” 换成 “Hello World”,那么原先的 “5” 这个 entry 会变成:

[02] [0b] [48 65 6c 6c 6f 20 57 6f 72 6c 64]
复制代码

至于为何的话,大家就对照的上面的本身试一遍,就当作练习题了。

相关文章
相关标签/搜索