压缩列表
ziplist
是一个经过特殊编码的双向链表,旨在提高内存效率。 它存储字符串和整数值,其中整数被编码为实际整数而不是一系列字符。ziplist
在任意一侧的Push
和Pop
操作时间复杂度都是O(1)O(1)O(1)。但是每个操作都需要重新分配ziplist
的内存,因此实际复杂性与ziplist
使用的内存量有关
创建
与前面几个数据结构不一样。Redis
并没有直接给出ziplist
的定义。但是我们可以从ziplist
的创建函数ziplistNew
中推出ziplist
的结构
#define ZIP_END 255
#define ZIPLIST_HEADER_SIZE (sizeof(uint32_t)*2+sizeof(uint16_t))
#define ZIPLIST_END_SIZE (sizeof(uint8_t))
#define ZIPLIST_BYTES(zl) (*((uint32_t*)(zl)))
#define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t))))
unsigned char *ziplistNew(void) {
unsigned int bytes = ZIPLIST_HEADER_SIZE+ZIPLIST_END_SIZE; // 头部大小+尾部大小 10bytes
unsigned char *zl = zmalloc(bytes); // 开辟空间
ZIPLIST_BYTES(zl) = intrev32ifbe(bytes); // 将[0,4]转为uint32_t类型 并且将(头部+尾部)所占字节写入
ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE); // 将[4,8]转为uint32_t类型 并且将头部大小写入
ZIPLIST_LENGTH(zl) = 0; // 将[8,10]转为uint16_t类型 并且将长度写入
zl[bytes-1] = ZIP_END; // 将ZIP_END写入尾部
return zl;
}
一个空的ziplist
的内存结构大致如下图
添加一个元素
一个空的ziplist
并不能确定其真的数据元素在内存中是如何布局的,解下来直接看添加元素的源码
// zlentry并不是数据实际的编码方式,而是用来解析ziplist中的数据的。这样更加方便对数据的操作
typedef struct zlentry {
unsigned int prevrawlensize; // 前一个元素编码长度所占字节数
unsigned int prevrawlen; // 前一个元素编码长度
unsigned int lensize; // 用于编码此节点类型/长度的字节
unsigned int len; // 用于表示节点实际的字节
unsigned int headersize; // prevrawlensize + lensize 用于表示节点头部的字节
unsigned char encoding; // 节点的编码类型 设置为ZIP_STR_*或ZIP_INT_*,具体取决于节点编码
unsigned char *p; // 第一个节点的地址指针
} zlentry;
unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {
// 在p处插入一个元素,元素的值为s,长度为slen
size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), reqlen, newlen;
unsigned int prevlensize, prevlen = 0;
size_t offset;
int nextdiff = 0;
unsigned char encoding = 0;
long long value = 123456789; // initialized to avoid warning
zlentry tail;
if (p[0] != ZIP_END) { // 说明p指向的是ziplist中间的某个元素的结束位置
// 获取p指向的元素的长度(这里涉及到一个变长整数的解码,有兴趣可以看一下宏的具体内容)
ZIP_DECODE_PREVLEN(p, prevlensize, prevlen);
} else { // p[0] == ZIP_END 说明p指向的是ziplist的最后一个字节
unsigned char *ptail = ZIPLIST_ENTRY_TAIL(zl); // ptail指向最后一个元素,如果ziplist是空,则指向尾部
if (ptail[0] != ZIP_END) {
// 说明ziplist不是空,求最后一个元素的长度。因为后续新元素要存储前一个元素的长度
prevlen = zipRawEntryLengthSafe(zl, curlen, ptail);
}
}
// reqlen为插入元素在ziplist占的总内存大小
// 1.数据元素的长度
if (zipTryEncoding(s,slen,&value,&encoding)) {
reqlen = zipIntSize(encoding);
} else {
reqlen = slen;
}
// 2.存储前一个元素长度需要的空间
reqlen += zipStorePrevEntryLength(NULL,prevlen);
// 3.存插入元素编码需要的空间
reqlen += zipStoreEntryEncoding(NULL,encoding,slen);
// 如果插入的新元素不是在ziplist的尾部,那么要确保新元素后的一个元素能存下新元素的长度,否则需要对后续元素扩容
int forcelarge = 0;
nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0;
if (nextdiff == -4 && reqlen < 4) {
nextdiff = 0;
forcelarge = 1;
}
offset = p-zl; // ziplist起始地址到插入点的偏移量
newlen = curlen+reqlen+nextdiff; // 新的ziplist所需要的内存字节数
zl = ziplistResize(zl,newlen); // 重新分配内存,并且最后一个字节以及设置为ZIP_END
p = zl+offset; // p指向即将插入元素的起始位置
if (p[0] != ZIP_END) {
// 如果插入的元素不是在ziplist的尾部,那么需要将后续元素往后移动
memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff);
if (forcelarge)
zipStorePrevEntryLengthLarge(p+reqlen,reqlen);
else
zipStorePrevEntryLength(p+reqlen,reqlen);
// 更新ziplist的偏移量
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+reqlen);
assert(zipEntrySafe(zl, newlen, p+reqlen, &tail, 1));
if (p[reqlen+tail.headersize+tail.len] != ZIP_END) {
// 新加入的元素后面还有元素,需要更新偏移量
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+nextdiff);
}
} else {
// 在ziplist的尾部插入元素,插入的元素就是尾部元素,直接更新ziplist头部保存的偏移量
ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(p-zl);
}
if (nextdiff != 0) {
// 说明插入元素后面的元素不能存储插入元素的长度,需要扩容 --- 注意这里可能会引起连锁更新
offset = p-zl;
zl = __ziplistCascadeUpdate(zl,p+reqlen);
p = zl+offset;
}
// 保存值
p += zipStorePrevEntryLength(p,prevlen);
p += zipStoreEntryEncoding(p,encoding,slen);
if (ZIP_IS_STR(encoding)) {
memcpy(p,s,slen);
} else {
zipSaveInteger(p,value,encoding);
}
ZIPLIST_INCR_LENGTH(zl,1);
return zl;
}
从插入一个元素的源码中,我们可以看出一个非空的ziplist
的内存结构如下
其他操作
其实在理解了ziplist
的内存结构之后,其他操作就很好理解了,这里就不再赘述了(主要是懒)。需要注意的是在删除元素和添加元素时ziplist
都有可能引发连锁更新