一.概述
压缩列表(ziplist)是列表和哈希对象中用于存储数据的底层实现之一,一般用于存储少量数据,且元素大小较小,如:较小整数,较短字符串,因为压缩列表可以使用较少的内存存储多种不同类型的数据,且有着平均性能为O(N)的增删性能。
二.压缩列表结构
一个压缩列表可以包含任意多个节点,其总体结构为:列表头 + 数据节点 + 列表尾节点(不含数据)。
- 列表头:由3部分组成,分别为:uint32_t zlbytes(整个压缩列表的字节数),uint32_t zltail(列表尾节点距压缩列表起始地址的偏移量,便于确定尾节点地址),uint16_t zllen(记录了压缩列表包含的节点数量,当达到最大值65535后不再增加,此时需遍历节点才可知列表大小)。
- 元素节点:可以有任意个元素节点,但是所占字节数不可超过uint32_t的最大值 - 4 - 4 - 2- 1。每个元素节点又包括3部分,前一节点长度 + 编码方式 + 数据内容(在后面叙述)。
- 表尾节点(uint8_t zlend):使用特殊值0xFF标记压锁列表末端(可以用该特殊值标记尾节点是由于数据节点的起始字节必小于等于0xFE)。
如上所述元素节点由3部分组成:前一节点长度 + 编码方式 + 数据内容(在后面叙述)
- 前一节点长度(previous_entry_length):该属性用于描述前一节点的大小,以便进行前序遍历,当前一节点大小小于254( 0xFE)时使用1字节表示,当大于等于254时使用5个字节表示,其中第一个字节为0xFE,表示前一字节大小大于254,而真正的实际长度存储在后四个字节中。【注】:除头节点外第一个字节小于等于)0xFE的是
- 编码方式(encoding):该属性用于表示后一个用于存储数据的属性中数据的类型及长度。
- 数据(content):该属性用于存储实际数据。
三.连锁更新
如前所述,每个数据节点都要保存前一节点的大小,且大小不同使用的字节数亦不同,因此,当插入删除节点时都可能连续更新(重新分配更大的空间)数个节点的previous_entry_length属性。最坏情况下需要更新n个节点,而每次重新分配空间的时间复杂度为O(N),因此最坏的时间复杂度为O(N^2)。,但这仅仅发生在连续的节点大小皆为250~253字节时(因为此时增加4个字节后>=254字节,从而导致了连锁反应),这种事件的概率极低,因此几乎不会影响性能,其平均时间复杂度为O(N)。
四.部分源码
压缩列表并不像其它数据结构一样有具体的数据结构描述,其实现由一组方法的集合和一组宏构成,如下所述:
1.压缩列表的创建
// 定义了压缩列表头的大小(由两个uint32_t和一个uint16_t组成)
#define ZIPLIST_HEADER_SIZE (sizeof(uint32_t)*2+sizeof(uint16_t))
// 定义了压缩列表为已一个0xFF(255)表示
#define ZIP_END 255
// 定位到存储压缩列表总字节位置
#define ZIPLIST_BYTES(zl) (*((uint32_t*)(zl)))
// 定位到存储到压缩列表尾偏移大小的位置
#define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t))))
// 定位到存储节点个数的位置
#define ZIPLIST_LENGTH(zl) (*((uint16_t*)((zl)+sizeof(uint32_t)*2)))
// 创建一个空的压缩列表
unsigned char *ziplistNew(void) {
unsigned int bytes = ZIPLIST_HEADER_SIZE+1; // 空压缩列表大小:压缩列表头 + 1字节表尾
unsigned char *zl = zmalloc(bytes); // 开辟空压缩列表的空间
ZIPLIST_BYTES(zl) = intrev32ifbe(bytes); // 设置压缩列表的总字节数
ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE); // 设置压缩列表尾节点偏移
ZIPLIST_LENGTH(zl) = 0; // 设置压缩列表节点数
zl[bytes-1] = ZIP_END; // 设置压缩压缩列表尾为0xFF
return zl;
}
//当创建完成后,空压缩列表的结构如下所示
|<---- ziplist header ---->|<-- end -->|
大小 4 字节 4 字节 2 字节 1 字节
+---------+--------+-------+-----------+
| zlbytes | zltail | zllen | zlend |
| | | | |
value | 1011 | 1010 | 0 | 1111 1111 |
+---------+--------+-------+-----------+
2.插入元素
// - zl : 指向压缩列表的首地址
// - p : 要插入的位置(插入在p之前)
// - s : 要插入元素的首地址
// - slen: 要插入元素的长度
static unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {
size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), reqlen;
unsigned int prevlensize, prevlen = 0;
size_t offset;
int nextdiff = 0;
unsigned char encoding = 0;
long long value = 123456789; /* initialized to avoid warning. Using a value
that is easy to see if for some reason
we use it uninitialized. */
zlentry tail;
// 用preclen保存插入位置前一节点的长度信息
if (p[0] != ZIP_END) { // 若不是插入在末尾之前
// 获取插入位置前一节点的长度保存在prelen中,prevlensize用于获取p所指节点用几个字节保存长度信息
ZIP_DECODE_PREVLEN(p, prevlensize, prevlen);
} else {
// ptail指向表尾节点,若列表为空,则ptail指向末尾,否则指向最后一个列表节点
unsigned char *ptail = ZIPLIST_ENTRY_TAIL(zl);
if (ptail[0] != ZIP_END) { // 列表不为空,即此时插入到尾端,需要知道前一节点的长度信息
// 计算ptail所指节点的长度,这是因为做为最后一个节点,没有后续节点保存其长度信息
prevlen = zipRawEntryLength(ptail);
}
}
// 判断能否将要存储的元素转化为整数编码,比如字符串“123456789”便可转化为一个整型再进行存储
if (zipTryEncoding(s,slen,&value,&encoding)) {
reqlen = zipIntSize(encoding); // 转化成整型后的字节长度
} else {
/* 'encoding' is untouched, however zipEncodeLength will use the
* string length to figure out how to encode it. */
reqlen = slen;
}
// 加上用于表示前一节点长度的字节大小(1或5字节)
reqlen += zipPrevEncodeLength(NULL,prevlen);
// 加上编码长度
reqlen += zipEncodeLength(NULL,encoding,slen);
// 此时reqlen为待插入节点的长度
// 检查插入位置的后一节点是否足以存储插入节点的长度信息,nextdiff存储了后一节点还需增大多少字节
nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0;
// 记录p到压缩列表头的偏移,因为扩展压缩列表后,压缩列表的地址可能会发生变化。
offset = p-zl;
// 扩展压缩列表,并设置了尾字节
zl = ziplistResize(zl,curlen+reqlen+nextdiff);
// p重写指向插入位置
p = zl+offset;
/* Apply memory move when necessary and update tail offset. */
if (p[0] != ZIP_END) {
// 【注】
// - 将p-nextdiff起之后的数据复制到p+reqlen地址处
// - 复制到p+reqlen是为了为新节点空出空间
// - 从p-nextdiff起开始复制是为了便于修改插入节点后的一个节点中存储的前一节节点长度信息
memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff);
// 设置插入节点之后的节点中存储的前向节点长度信息
zipPrevEncodeLength(p+reqlen,reqlen);
// 重置到尾节点的偏移
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+reqlen);
/* When the tail contains more than one entry, we need to take
* "nextdiff" in account as well. Otherwise, a change in the
* size of prevlen doesn't have an effect on the *tail* offset. */
tail = zipEntry(p+reqlen);
if (p[reqlen+tail.headersize+tail.len] != ZIP_END) {
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+nextdiff);
}
} else {
/* This element will be the new tail. */
ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(p-zl);
}
// - 若nextdiff不为0则说明插入节点后一节点变长了,
// - 因此需调用__ziplistCascadeUpdate函数循环检查后续节点,直至某个节点无需增长
if (nextdiff != 0) {
offset = p-zl;
zl = __ziplistCascadeUpdate(zl,p+reqlen);
p = zl+offset;
}
/* Write the entry */
p += zipPrevEncodeLength(p,prevlen);
p += zipEncodeLength(p,encoding,slen);
if (ZIP_IS_STR(encoding)) {
memcpy(p,s,slen);
} else {
zipSaveInteger(p,value,encoding);
}
ZIPLIST_INCR_LENGTH(zl,1);
return zl;
}
// - zl : 指向压缩列表的首地址
// - s : 要插入元素的首字节
// - slen : 要插入元素的大小
// - where: 元素的插入位置
unsigned char *ziplistPush(unsigned char *zl, unsigned char *s, unsigned int slen, int where) {
unsigned char *p;
// 根据where参数确定是将新值插入首部还是尾部
p = (where == ZIPLIST_HEAD) ? ZIPLIST_ENTRY_HEAD(zl) : ZIPLIST_ENTRY_END(zl);
return __ziplistInsert(zl,p,s,slen);
}
3.删除元素
删除元素的大致思想与插入元素相同,此处不再赘述