以下涉及到的源码均为redis5.0-rc3版本的代码【点击查看官方源码】
压缩列表(ziplist)
- 压缩列表是一个特殊编码的列表,为节约内存而开发。
- 压缩列表可以保存字符串和整型值,其中整数存放时仍是整数,而不会变成字符。
- 压缩列表允许push和pop操作在时间复杂度为O(1)的情况下完成。但是每个操作都可能需要重新分配ziplist使用的内存,所以实际的复杂性受ziplist使用内存量及其变动的影响。
- 压缩列表在redis中是列表键和哈希键的底层实现之一。
- 由于压缩列表本就为节约内存而开发,且其改动的操作都可能会重新分配entry的内存,导致操作的时间复杂度增加,所以为了性能上不受影响,其适用于存储小整数值和较短的字符串。
数据结构
压缩列表
压缩列表的数据结构是在ziplist.c文件中注释说明中这样描述的:
1 | <zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend> |
其中各部分的含义如下:
- zlbytes
值类型为uint32_t,占用4个字节。是一个无符号整型值,保存着整个压缩列表占用的字节数(包含自己占用的4个字节)。此值的作用在于当对ziplist的内存进行重新分配的时候直接使用该值,而无需进行遍历整个ziplist; - zltail
值类型为uint32_t,占用4个字节。列表最后一个节点的地址偏移量,这样就可以使得进行pop操作的时候无需遍历整个ziplist; - zllen
值类型为uint16_t,占用2个字节。记录整个ziplist中节点的总数,当其大小大于2^16-2的时候,zllen=2^16-1,此时,便需要遍历整个ziplist才能确定节点的数量; - entry
ziplist的节点 - zlend
值类型为uint8_t,占用1个字节。值为255,表示ziplist结束的位置,当一个字节被设置值为255时即表示后续不会再有节点;
下面用图来举例说明:
压缩列表节点
压缩列表节点的组成一般情况下如下所示:
1 | <prevlen> <encoding> <entry-data> |
然而,如果节点存储的是小整型时,节点就不会有
1 | <prevlen> <encoding> |
由上可见,每个entry节点都会有前缀信息,且其前缀都由两方面信息组成:prvlen、encoding。
prvlen
- 代表前一个节点占用的字节长度,方便ziplist从后往前遍历。如p1代表压缩列表地址,p2代表最后一个节点地址,p3代表倒数第二个节点地址,由此可见:
1 | p2 = p1 + zltail |
如果长度小于254字节,prvlen则占用1个字节。
1
<prevlen from 0 to 253> <encoding> <entry>
如果长度大于254字节,prvlen则占用5个字节长度,第一个字节代值为254(FE),后面的四个字节用于保存前一个节点的长度。
1
0xFE <4 bytes unsigned little endian prevlen> <encoding> <entry>
encoding
- 表示当前节点的编码,可以是integer或者string。
- 当其节点存储的是一个string时,encoding的第一个字节的前2个bit(00、01、10)代表了ecoding的类型,其他的bit则表示string的长度。
- 当encoding的第一个字节的前2个bit为11时,代表entry节点存储的是整数,且后续的2个bit则可以用来表示integer的值(即节点存储小整数的时候)
下面直接贴身源码中的例子,来举例当类型不同时的encoding的样子。
1 | //---------------字符串类型---------------// |
压缩列表存储样例
这里用源码中的两个例子来具体表示压缩列表的存储情况,如下所示:
1 | //-----------------整数类型------------// |
API
在ziplist.h头文件中给出了如下api:
1 | //创建压缩列表 |
下面将只对节点的定义,以及节点的插入和删除操作的源码做介绍,其余的若想要了解可去浏览源码。而为什么要单独只介绍删除和插入两个操作呢?因为其涉及到压缩列表的一个影响性能的重要操作——空间扩展
entry的定义
1 | typedef struct zlentry { |
删除操作
1 | //删除元素为num的节点 |
插入操作
1 | //插入节点p |
空间扩展
1 | /* When an entry is inserted, we need to set the prevlen field of the next |