Redis数据结构之ZipList
什么是ZipList
ZipList 也叫压缩链表,可以被看作是一种连续空间的 “双端链表”
;
但它并不是链表,它并不存储指针,通过字节长度和偏移量之间的计算来寻址,查找时间复杂度为O(n)。所以:它是一种以时间换空间的数据结构。
首尾的操作复杂度为O(1);
但是压缩列表通过申请连续的内存空间存储,节省空间,这是它优点,同时也是它缺点,因为当数据量越来越大时,并不能保证能申请到大的连续的内存空间,查询性能也会下降(O(n)),所以一般当数据量达到一定程度时,需要转成其他存储结构。比如quicklist、hash等。
ZipList 结构
ZipList 头部由三个固定长度的属性(zlbytes、zltail、zllen)(长度分别是4字节、4字节、2字节)组成;
尾部由1个字节长度的 zlend (内容是16进制的0xff ) , 标记尾部;
中间就是存储了数据部分的节点entry,长度不固定,由存储的内容决定;
结构大致长这样:
zlbytes | zltail | zllen | entry | entry | zlend |
---|---|---|---|---|---|
4字节 | 4字节 | 2字节 | 不确定 | 不确定 | 1字节 |
ZipList各属性的含义
属性 | 长度 | 含义 |
---|---|---|
zlbytes | 4字节 | 整个压缩链表占用的内存字节数 |
zltail | 4字节 | 压缩列表表尾节点距离压缩列表的起始地址的字节数(最后一个entry的起始地址 到压缩列表起始地址 的字节数),即偏移量 |
zllen | 2字节 | 压缩列表包含的节点数量。最大为 UINT16_MAX (65534),如果超出,仍只能记录为65535,但真实数量需要遍历整个压缩列表 |
entry | 不确定 | 保存内容的节点,长度与保存的内容有关 |
zlend | 1字节 | 保存了16进制的 (0xff) 也就是十进制的 255,标记列表末端 |
列表节点entry
压缩列表并没有存储指针,那么压缩列表各个节点之间是如何做到两端都能访问的呢?
这就是每个列表节点entry的结构厉害之处,每个entry保存了三个属性,分别是:
- previous_entry_length(前一个节点的长度)
- 如果前一个节点长度小于254 (<254),则用1个字节保存这个长度
- 如果前一个节点长度大于等于254 (>=254),则采用5个字节长度保存,其中第一个字节为0xfe,后面四个节点才是真实的数据长度;
- encoding(编码)
- 记录content内容的
数据类型
以及长度
,占1、2或5个字节长度 - 以’00’, ‘01’, ‘10’ 开头,那么content保存的是字符串
- 以 ‘11’ 开头则保存的是整数
- 记录content内容的
- contents(保存的内容)
- 保存数据的地方,可以是字符串或者整数。
属性 | previous_entry_length | encoding | contents |
含义 | 前一个节点长度 | content的数据类型以及长度 | 数据内容 |
大小 | 1 或 5 个字节 | 1 或 2 或 5个字节 | encoding中存储 |
因为254是zlend保存的,所以previous_entry_length在>=254时就要用另一张方法区别
从前往后:压缩列表起始地址+10个字节,就是第一个entry的起始地址;
从后往前:压缩列表起始地址+zltail,就是最后一个entry的起始地址;
连锁更新
连锁更新发生的概率较低,在新增或者删除的时候会发生;
发生的原因是entry中的previous_entry_length存储机制;
previous_entry_length中存储的是前一个节点的长度,当前一个节点长度 <254 时 previous_entry_length只占一个字节,>= 254 时占5个字节;
假设现在有连续 n 个列表节点entry,并且每个entry长度都在 250 ~ 253之间(包括);
此时向这最前面的 entry1 前插入一个 >= 254长度节点
,那么 entry1 节点的previous_entry_length就要变成5字节长度,entry1 节点长度+4 >= 254
了,所以下一节点entry2的previous_entry_length也要变成5个字节长度
,entry2长度也+4
,又 >= 254了,所以entry3的previous_entry_length…
这种连续的空间扩张,被称为连锁更新,发生的概率非常低,但是由于要不断地去申请空间,和释放内存,所以会严重影响性能;