文章目录
- ZipList(压缩列表)
- 概念
- `ZipList`的结构
- `Entry`的内部结构
- `previous_entry_length`
- `Encoding`
- 存储字符串
- 存储整数
- `content`
- `ZipList`会存在的问题
- 查询中间数据
- 连锁更新
- 总结
ZipList(压缩列表)
概念
ZipList
是一种特殊的"双端链表",由一系列特殊编码的连续内存块组成(所以本质上并不是链表,但是具有链表的特性).可以在任意一段进行压入/弹出操作,并且该操作的时间复杂度为O(1)
.
ZipList
的结构
各属性含义:
这里我们看到,
entry
长度是不定的,也就是说,每个entry
节点的大小并不是统一的,而是根据数据的大小来进行动态变化,这也是ZipList
可以节省空间的关键
那么这里会引入一个问题:ZipList
是如何进行寻址的呢?
这与entry的内部结构有关系.
Entry
的内部结构
previous_entry_length
:前一个节点的长度,占1
个或者5
个字节- 如果前一个节点的长度小于
254
个字节,那么就采用1
个字节来保存这个长度值 - 如果前一个节点的长度大于
254
个字节,那么就采用5
个字节来保存这个长度值,并且第一个字节为0xfe
,后四个字节才是真实的长数据
- 如果前一个节点的长度小于
encoding
:编码属性,记录当前content
的数据类型(字符串还是整数)以及长度,占用1
个,2
个或者5
个字节contents
:负责保存节点的数据,可以是字符串或者整数
Entry
为什么这么设计:
这样设计,保证了正序遍历和逆序遍历.
正序遍历:正序遍历比较容易理解,因为ZipList
是连续的内存空间,所以此处只需要从第一个entry
起始地址开始,不断移动指针,便可以遍历完整个ZipList
.
逆序遍历:
previous_entry_length
和encoding
都采用小端字节序来进行存储的.
- 小端字节序:低位字节在前,高位字节在后.例如:数值
0x1234
,采用小端字节序来进行存储时,实际的存储值为0x3412
.
previous_entry_length
previous_entry_length
:记录前一个节点的长度.
- 此时我们要存储字符串
"ab"
时,前面没有节点,所以此处为0
,转换为16
进制,就应该为0x00
.
Encoding
Encoding
:编码属性,记录当前content
的数据类型(字符串还是整数)以及长度,占用1
个,2
个或者5
个字节
存储字符串
首先是存储字符串的表示方式:
如果encoding
是以00
,01
,10
开头,则证明content
是字符串
00
:表示encoding
的长度为1
字节01
:表示encoding
的长度为2
字节10
:表示encoding
的长度为5
字节,且第1
个字节不表示长度
例如还是存储"ab"
,此时"ab"
占2
字节,所以entry
只需要占1
字节即可,所以二进制形式应该表示为00000010
,转化为16
进制,表示为0x02
存储整数
如果encoding
是以11
开始,则证明content
是整数,且encoding
固定只占用1
个字节
这里我们需要关注最后一种存储方式1111****
,这里适用于比较小的整数,比如存储2
,这时并不需要将数据内容存放到content
中,只需要存储到encoding
中即可,此时的encoding
表示为11110011
.转化为16
进制,表示为0xf3
,这样做的目的也是为了节省内存空间.
content
此处便是存储数据 的位置,比较简单,比如"ab"
,此处转化为16
进制数据后,可以得到:0x61
和0x62
.
综上所述:
ZipList
使用场景非常广泛,原因是:ZipList
在节省内存方面作出了很多努力.
但ZipList
只能做到正序遍历和倒序遍历,所以如果查询的内容位于中间,这就会带来数据遍历效率低的问题.也就是说:ZipList
的数据查询和链表是一样的,只不过在节省空间方面,ZipList
会更加优良
ZipList
会存在的问题
查询中间数据
我们了解到,ZipList
相当于一个具有连续内存空间的双向链表,那么就会具有双向链表中存在的中间数据遍历效率低的问题,因为ZipList
并不能直接进行
连锁更新
ZipList
某种特殊情况下产生的连续空间扩展操作称之为连锁更新
现在,假设我们有N
个连续的,长度为250~253
字节之间的entry
,因此entry
的previous_entry_length
属性用1
个字节即可表示,如下图所示:
此时,如果我们在第一个节点前面加上一个260
字节长度的entry
此时,之前的第一个entry
中的pre_entry_length
就需要从占用1
字节变为占用5
字节,此时后续的entry
均需要发生这样的改变.这样就带来了连锁更新的情况
但是,连锁更新的情况,在Redis
当中,并没有进行解决,原因是这种特殊情况是比较罕见的,需要同时满足:要有连续的,N
个字节数在250~253
之间长度的entry
.
总结
- 压缩列表可以看做是一种连续内存空间的"双向链表"
- 列表的节点之间不是通过指针链接,而是记录上一节点和本节点的长度来寻址,内存占用较低
- 如果列表数据过多,导致链表过长,可能影响查询性能
- 增或者删除较大数据时,都可能会发生连锁更新的问题