阅读 3654

【redis前传】将内存节省到极致的一种数据结构ziplist |8月更文挑战

前言

  • 在我们讲解list结构的时候提到了一种特殊的结构ziplist ,俗称压缩列表。同时他也是hash结构和list结构采用的底层结构之一。他的出现是为了节省内存的一种结构。

  • 当一个list结构的数据中只包含少量列表项且里面元素是小整数或者是短的字符串时redis底层就会使用ziplist来存储数据

image-20210712155337607

结构

  • redis的ziplist是一块连续内存块。我们可以简单理解成数组,相比较数组而言她却多了很多对节点关联的描述,比如说数组总长、最后一个地址偏移量、上一个节点的长度等等信息。

image-20210715152827696

  • 上述是redis源码中对ziplist结构的一段描述!根据图中圈出部分我们可以简单的理解ziplist的总体结构

image-20210715161444888

  • 那么这些分别代表着什么作用呢?他们又是如何将内容存储在连续内存块中的呢?

image-20210715161610320

  • 每一块都有固定的内存空间表示这他的作用。只有entry因为是存储节点的所用他的长度是动态的。
字段类型长度作用
zlbytesunit32_t四字节,32位整个ziplist占用字节数。
zltailunit32_t四字节,32位尾结点距离起始位置偏移量。这里需要尾结点句柄距离头部偏移量
zllenunit16_t二字节,16位节点个数 。 16位表示最大值65535 。 当元素超过65535时我们只能遍历获取节点个数
entry ...entry动态
zlendunit8_t一字节,8位固定值0XFF 。 用于表示标记位
  • 下面我们来看看一个列子

image-20210715163317262

节点

image-20210715163445170

  • 针对redis源码中我做了解释。他的内存结构如图

image-20210715163526536

  • 上述头部是有一定关联的。我们可以将头部进行简单规划为一个head

image-20210715163629443

  • 关于previous_entry就表示这前一个节点的长度。我们根据他就可以逆推值前一节点。这也解释了我们如何定位ziplist中的元素。我们可以根据zlbytes和zltail获取到尾结点。在根据尾结点的previous_entry获取前一节点。

previous_entry

  • 这部分我们简单理解他会占用5字节(最大情况) 。 redis考虑到内存的紧凑型这地方会优先使用1字节来记录当1字节不满足情况下类似于intset一样这时候会用5字节来表示。5字节去除第一字节固定值正好能表示32数字是我们int类型的数字范文。
  • 这里也正好呼应我开头说的ziplist的使用场景。ziplist主要是用来存储小整数或者是短字符。他们很小或者很短的情况下entry的节点长度也不会太大,极大的情况下8位就可以表示了他们的长度!
  • 这里也可以看出redis真的对内存把握的死死的。当超出了254redis的previous_entry相当于升级了扩展成了5字节,其中第一字节设置为0XFE固定值。而后面四字节表示前一节点的实际长度

疑问?为什么固定值是0XFE

  • 首先只有第一字节被设置成固定值,1个字节在16进制中常见的是FF用来表示。但是这里为什么不是FF 。 因为在ziplist中0XFF用于表示zlend标记。所以这里前移用0XFE作为entry的标记。

encoding

  • encoding属性记录了节点的数据的类型以及长度!简单描述就是记录了内容的特征

image-20210715164927933

  • 上述是redis源码对entry的一个解释。我针对他做了一份表格统计。
编码长度(字节)
00aaaaaa1长度<=63 字符串
01pppppp|qqqqqqqq2长度<=16383 字符串
10xxxxxx|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt5长度<=4294967295 字符串
110000001int16
110100001int32
111000001int64
11110000124位有符号整数
1111111018位有符号整数
1111xxxx (xxxx的取值范围【0000 , 1101))10~12
11111111ziplist结束位
  • 所有的encoding除了11111111固定结束位,其他所有的前两位用于表示数据类型

  • 前两位11表示整数;00表示字节长的字符串;01表示字节字符串;10表示5字节字符串

  • 计算长度就是encoding去除前两位后表示的数字就是属性的长度。

00001000

  • 首先开头是00,我们根据手册可以知道entry中存储的是1字节的字符串,那么他的长度是多少的呢?是二进制的1000即长度为8.
  • 8位也就是1个字节,也就是说他表示的节点内部存储的内容是1字节内容,可能是a

01000001,00000000

  • 首先开头是01表示长度为2字节的字符。去除开头两位翻译成十进制是256。表示32字节的内容。我们可以理解成该entry存储了32个英文字母
  • 后续的就不在进行举例。根据encoding我们就可以确定entry的内容及长度。

内容

  • 节点的p是用于保存实际内容的。根据上面encoding的介绍我们了解到内容可以是字符串也可以是整数。
  • 所以在计算entry的长度时我们是不需要看实际内容的长度的,因为他的长度已经在encoding里了。

回到从前

  • 还记得上面我们展示的ziplist草图吗,在哪里我们标注了zlbytes、zltail、zllen 、 entryx 、 zlend等占位说明。现在我们将完善这幅草图

image-20210715183929044

  • 在上面这张图我们加入了三个节点的内存图示。在这里我们将清楚的了解entry的结构。但是最终的内部还是将头部的展示进行了细分

连锁更新

image-20210715184147234

  • 在上面我们知道previous_entry是用于标记前一节点的长度,而且当时我们也说了redis为了节省内存会先尝试用1字节来表示当长度超出254时才会扩容为4字节长度。这就意味着我们每个entry节点长度都是动态的,换言之我们每个entry都会受到前一节点的影响

  • 比如说现在我们的entry的长度都在250~254之间。如下图

image-20210715184959823

  • 这个时候我们需要在头部添加一个长度为254的节点。因为该节点254所以我们上面第一个节点的previous_entry将会由1变成了5字节。从而影响到整个entry由原来的250字节变成了254字节。因为第一个节点变成了254字节从而又递归影响了原来第二节点!

image-20210715185248265

  • redis将这种添加节点到头部引发的连锁反应称之为连锁更新。同样的道理我们在删除的时候也会遇到类似情况,应该这两种情况统称为连锁更新!

image-20210715185521950

总结

  • 首先我们从结构上解析了ziplist的属性,然后从ziplist中主要角色entry出发继续解析内部结构说明。
  • 在entry中我们大体分成三部分。其中previous_entry因为取决于前一节点,从而牵扯出redis重要问题【连锁更新】 。
  • 连锁更新发生的概览很是罕见。每个节点长度在25-~253范围内才会发生连锁更新,这本身概览就很低。其次在添加的节点是254本身也很低了。因为ziplist主要是小整数或者短字符使用的。
  • 而且当节点很少的情况下即使发生连锁更新我们也是可以接受的。毕竟在内存中。
  • 综上!ziplist主要使用在小整数、短字符等数据量少的情况最佳!!!

求个赞,谢谢大爷!!!

文章分类
后端