ziplist
虽然不维护前后节点的指针,但是它却维护了上一个节点的长度和当前节点的长度,然后每次通过长度来计算出前后节点的位置。既然涉及到了计算,那么相对于直接存储指针的方式肯定有性能上的损耗,这就是一种典型的用时间来换取空间的做法。因为每次读取前后节点都需要经过计算才能得到前后节点的位置,所以会消耗更多的时间,而在 Redis
中,一个指针是占了 8
个字节,但是大部分情况下,如果直接存储长度是达不到 8
个字节的,所以采用存储长度的设计方式在大部分场景下是可以节省内存空间的。
ziplist 的存储结构
ziplist
的组成结构为:
<zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>
其中 zlbytes
,zltail
,zllen
为 ziplist
的 head
部分,entry
为 ziplist
的 entries
部分,每一个 entry
代表一个数据,最后 zlend
表示 ziplist
的 end
部分,如下图所示:
ziplist
中每个属性代表的含义如下表格所示:
属性 | 类型 | 长 度 | 说明 |
---|---|---|---|
zlbytes | uint32_t | 4字节 | 记录压缩列表占用内存字节数(包括本身所占用的 4 个字节)。 |
zltail | uint32_t | 4字节 | 记录压缩列表尾节点距离压缩列表的起始地址有多少个字节(通过这个值可以计算出尾节点的地址) |
zllen | uint16_t | 2字节 | 记录压缩列表中包含的节点数量,当列表值超过可以存储的最大值(65535 )时,此值固定存储 65535 (即 2 的 16 次方 减 1 ),因此此时需要遍历整个压缩列表才能计算出真实节点数。 |
entry | 节点 | - | 压缩列表中的各个节点,长度由存储的实际数据决定。 |
zlend | uint8_t | 1字节 | 特殊字符 0xFF (即十进制 255 ),用来标记压缩列表的末端(其他正常的节点没有被标记为 255 的,因为 255 用来标识末尾,后面可以看到,正常节点都是标记为 254 )。 |
entry 存储结构
ziplist
的 head
和 end
存的都是长度和标记,而 entry
存储的是具体元素,这又是经过特殊的设计的一种存储格式,每个 entry
都以包含两段信息的元数据作为前缀,每一个 entry
的组成结构为:
<prevlen> <encoding> <entry-data>
prevlen
prevlen
属性存储了前一个 entry
的长度,通过此属性能够从后到前遍历列表。 prevlen
属性的长度可能是 1
字节也可能是 5
字节:
- 当链表的前一个
entry
占用字节数小于254
,此时prevlen
只用1
个字节进行表示。
<prevlen from 0 to 253> <encoding> <entry>
- 当链表的前一个
entry
占用字节数大于等于254
,此时prevlen
用5
个字节来表示,其中第1
个字节的值固定是254
(相当于是一个标记,代表后面跟了一个更大的值),后面4
个字节才是真正存储前一个entry
的占用字节数。
0xFE <4 bytes unsigned little endian prevlen> <encoding> <entry>
注意:1
个字节完全你能存储 255
的大小,之所以只取到 254
是因为 zlend
就是固定的 255
,所以 255
这个数要用来判断是否是 ziplist
的结尾。
encoding
encoding
属性存储了当前 entry
所保存数据的类型以及长度。encoding
长度为 1
字节,2
字节或者 5
字节长。前面我们提到,每一个 entry
中可以保存字节数组和整数,而 encoding
属性的第 1
个字节就是用来确定当前 entry
存储的是整数还是字节数组。当存储整数时,第 1
个字节的前两位总是 11
,而存储字节数组时,则可能是 00
、01
和 10
三种中的一种。
- 当存储整数时,第
1
个字节的前2
位固定为11
,其他位则用来记录整数值的类型或者整数值(下表所示的编码中前两位均为11
):
编码 | 长度 | entry保存的数据 |
---|---|---|
11000000 | 1字节 | int16_t类型整数 |
11010000 | 1字节 | int32_t类型整数 |
11100000 | 1字节 | int64_t类型整数 |
11110000 | 1字节 | 24位有符号整数 |
11111110 | 1字节 | 8位有符号整数 |
1111xxxx | 1字节 | xxxx 代表区间 0001-1101 ,存储了一个介于 0-12 之间的整数,此时 entry-data 属性被省略 |
注意:xxxx
四位编码范围是 0000-1111
,但是 0000
,1111
和 1110
已经被表格中前面表示的数据类型占用了,所以实际上的范围是 0001-1101
,此时能保存数据 1-13
,再减去 1
之后范围就是 0-12
。至于为什么要减去 1
是从使用习惯来说 0
是一个非常常用的数据,所以才会选择统一减去 1
来存储一个 0-12
的区间而不是直接存储 1-13
的区间。
- 当存储字节数组时,第
1
个字节的前2
位为00
、01
或者10
,其他位则用来记录字节数组的长度:
编码 | 长度 | entry保存的数据 |
---|---|---|
00pppppp | 1字节 | 长度小于等于 63 字节(6 位)的字节数组 |
01pppppp qqqqqqqq | 2字节 | 长度小于等于 16383 字节(14 位)的字节数组 |
10000000 qqqqqqqq rrrrrrrr ssssssss tttttttt | 5字节 | 长度小于等于 2 的 32 次方减 1 (32 位)的字节数组,其中第 1 个字节的后 6 位设置为 0 ,暂时没有用到,后面的 32 位(4 个字节)存储了数据 |
entry-data
entry-data
存储的是具体数据。当存储小整数(0-12
)时,因为 encoding
就是数据本身,此时 entry-data
部分会被省略,省略了 entry-data
部分之后的 ziplist
中的 entry
结构如下:
<prevlen> <encoding>
压缩列表中 entry
的数据结构定义如下(源码 ziplist.c
文件内),当然实际存储并没有直接使用到这个结构定义,这个结构只是用来接收数据,所以大家了解一下就可以了:
typedef struct zlentry {
unsigned int prevrawlensize;//存储prevrawlen所占用的字节数
unsigned int prevrawlen;//存储上一个链表节点需要的字节数
unsigned int lensize;//存储len所占用的字节数
unsigned int len;//存储链表当前节点的字节数
unsigned int headersize;//当前链表节点的头部大小(prevrawlensize + lensize)即非数据域的大小
unsigned char encoding;//编码方式
unsigned char *p;//指向当前节点的起始位置(因为列表内的数据也是一个字符串对象)
} zlentry;
ziplist 数据示例
上面讲解了大半天,可能大家都觉得枯燥无味了,也可能会觉得云里雾里,这个没有关系,这些只要心里有个概念,用到的时候再查询对应资料就可以了,并不需要全部记住,接下来让我们一起通过两个例子来体会一下 ziplist
到底是如何来组织存储数据的。
下面就是一个压缩列表的存储示例,这个压缩列表里面存储了 2
个节点,节点中存储的是整数 2
和 5
:
[0f 00 00 00] [0c 00 00 00] [02 00] [00 f3] [02 f6] [ff]
| | | | | |
zlbytes zltail zllen "2" "5" end
# **写在最后**
以上就是我的面试过程,为了这次面试,也收集了很多的面试题,反正我已经面过了,那就免费分享出来吧!
需要的朋友:**关注一下,然后[点击这里即可](https://gitee.com/vip204888/java-p7)免费领取**
以下是部分面试题截图
也收集了很多的面试题,反正我已经面过了,那就免费分享出来吧!
需要的朋友:**关注一下,然后[点击这里即可](https://gitee.com/vip204888/java-p7)免费领取**
以下是部分面试题截图
![Java程序员秋招三面蚂蚁金服,我总结了所有面试题,也不过如此](https://img-blog.csdnimg.cn/img_convert/3f90ed90c526671889485f5c128cf89e.png)