zipList是redis为了节约内存开发而设计的,zipList是内存连续的。
zipList的数据结构
下面图即为zipLst的数据结构,其中:
-
zlbytes: 记录整个压缩列表的内存的数量,主要是用来进行对这个压缩列表的内存重新分配
-
zltail: 记录的是这个zipList的起始地址到尾节点的偏移量,通过这个偏移量能直接找到这个链表的尾节点
-
zlen: 主要存储的是整个压缩列表的节点的数量,需要注意的是它是两个字节,当其字节超过65535(包括)时,则需要通过遍历才能找到这个zipList的长度
-
zlend: 这个数据为255,表示的是结束的节点,可以用来判断这个列表是否为空
-
entry: 是存储数据的节点,用于具体存储数据。
- prevLength: 表示当前节点的前一个节点的长度,通过这个长度即可向前访问节点,需要注意的是它的长度是可变的,当其大小小于254时,即用一个字节表示,当其大于等于254时,即用5个字节表示。
- encoding 这表示的是后面的content的了类型是int还是字节数组,以及后续节点的长度信息
- content: 这表示的则是具体的数据内容。
coding
coding是用来表示content的内容是int还是byte数组,以及content的长度的,首先它通过conding的第一个字节的前两位来确认编码方式。
编码 | 含义 | 编码长度 |
---|---|---|
00 | content是长度小于0x3f(这个字节的剩余6位就可表示)的字节数组 | 1 |
01 | content是长度从(0x3f,0x0x3fff]之间的字节数组 | 2 |
10 | content是长度为32位的字节数组 | 5 |
11 | content是一个整数 | 不确定 |
可以看到当前两位的数值位00,01,10时,content为字节数组,当前两位为11时,则表示content为一个整数数值。而这个整数数值到底是多少位的,则需要这个字节后续的编码来确定。
编码 | content的值 |
---|---|
11111110 | 8位有符号整数 |
11000000 | 16位有符号整数 |
11110000 | 24位有符号整数 |
11010000 | 32位有符号整数 |
11100000 | 64位整数 |
11110000-11111100 | 表示的是0-12的整数值 |
zipList的具体实现
对于zipList的主要方法是insert,delete和find这三种操作,需要注意的是zipList没有提供update方法,对zipList的节点更新就是将原来的节点删除,然后加入新的节点。
insert方法
插入方法主要有下面几个步骤。
- 首先根据传入数据计算出其对应的conding,以及获取其对应的需要插入的节点,然后获取这个节点的preLength,通过prevLength,encoding,content这三个数据即可知道此次插入节点的长度,即为下图中橙色系欸但表示的长度。
- 然后对当前的zipList进行重新的内存分配,并且将当前节点向后移动这个空间的长度来用于存储新的数据。需要注意的是这个新分配出来的空白内存可能不止新节点的总长度(也有可能比它小),因为新插入的节点的长度可能比当前节点原来的节点的preLength不同,所以有可能这个preLength的编码长度可能会发生改变,因此此处的红色部分即为多分配的用于存储新的当前节点的preLength用的
- 然后就是更新当前节点的preLength节点为新插入的节点的长度,更新ztail的偏移量为新的节点,并且将新节点数据加入到分配的内存中。
- 级联更新后续的preLength,这个操作其实是在将节点数据加入到内存之前进行的,不过这个顺序应该影响不大,因为之前改变了当前插入节点的preLength,这个preLength的值的编码大小可能会发生变化,从而引起当前插入节点的后面节点的preLength的变化,从而需要迭代的将后面的节点的preLength。需要注意的是只有preLength的编码变长才会重新分配内存(即从一个字节变成5个字节),当编码变短时,依然用5个字节来表示这个新的数据,而不会对内存进行重新分配
static unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {
// 记录当前 ziplist 的长度
size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), reqlen, prevlen = 0;
size_t offset;
//表示的是p所指向的节点的前置节点的length的编码的字节长度差
int nextdiff = 0;
unsigned char encoding = 0;
long long value = 123456789;
zlentry entry, tail;
/* Find out prevlen for the entry that is inserted. */
if (p[0] != ZIP_END) {
// 如果 p[0] 不指向列表末端,说明列表非空,并且 p 正指向列表的其中一个节点
// 那么取出 p 所指向节点的信息,并将它保存到 entry 结构中
// 然后用 prevlen 变量记录前置节点的长度
// (当插入新节点之后 p 所指向的节点就成了新节点的前置节点)
// T = O(1)
entry = zipEntry(p);
prevlen = entry.prevrawlen;
} else {
// 如果 p 指向表尾末端,那么程序需要检查列表是否为:
// 1)如果 ptail 也指向 ZIP_END ,那么列表为空;
// 2)如果列表不为空,那么 ptail 将指向列表的最后一个节点。
unsigned char *ptail = ZIPLIST_ENTRY_TAIL(zl);
if (ptail[0] != ZIP_END) {
// 表尾节点为新节点的前置节点
// 取出表尾节点的长度
// T = O(1)
prevlen = zipRawEntryLength(ptail);
}
}
// 尝试看能否将输入字符串转换为整数,如果成功的话:
// 1)value 将保存转换后的整数值
// 2)encoding 则保存适用于 value 的编码方式
// 无论使用什么编码, reqlen 都保存节点值的长度
// T = O(N)
if (zipTryEncoding(s,slen,&value,&encoding)) {
/* 'encoding' is set to the appropriate integer encoding */
reqlen = zipIntSize(encoding);
} else {
reqlen = slen;
}
// 计算编码前置节点的长度所需的大小
// T = O(1)
reqlen += zipPrevEncodeLength(NULL,prevlen);
// 计算编码当前节点值所需的大小
// T = O(1)
reqlen += zipEncodeLength(NULL,encoding,slen);
// 只要新节点不是被添加到列表末端,
// 那么程序就需要检查看 p 所指向的节点(的 header)能否编码新节点的长度。
// nextdiff 保存了新旧编码之间的字节大小差,如果这个值大于 0
// 那么说明需要对 p 所指向的节点(的 header )进行扩展
// T = O(1)
nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0;
// 因为重分配空间可能会改变 zl 的地址
// 所以在分配之前,需要记录 zl 到 p 的偏移量,然后在分配之后依靠偏移量还原 p
offset = p-zl;
// curlen 是 ziplist 原来的长度
// reqlen 是整个新节点的长度
// nextdiff 是新节点的后继节点扩展 header 的长度(要么 0 字节,要么 4 个字节)
// T = O(N)
zl = ziplistResize(zl,curlen+reqlen+nextdiff);
p = zl+offset;
if (p[0] != ZIP_END) {
// 新元素之后还有节点,因为新元素的加入,需要对这些原有节点进行调整
// 移动现有元素,为新元素的插入空间腾出位置
// T = O(N)
//这里是将p-nextdiff位置开始向后移动到p-reqlen地位置
memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff);
// 将新节点的长度编码至后置节点
// p+reqlen 定位到后置节点
// reqlen 是新节点的长度
// T = O(1)
zipPrevEncodeLength(p+reqlen,reqlen);
// 更新到达表尾的偏移量,将新节点的长度也算上
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+reqlen);
// 如果新节点的后面有多于一个节点
// 那么程序需要将 nextdiff 记录的字节数也计算到表尾偏移量中
// 这样才能让表尾偏移量正确对齐表尾节点
// T = O(1)
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 {
// 新元素是新的表尾节点
ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(p-zl);
}
// 当 nextdiff != 0 时,新节点的后继节点的(header 部分)长度已经被改变,
// 所以需要级联地更新后续的节点
if (nextdiff != 0) {
offset = p-zl;
// T = O(N^2)
zl = __ziplistCascadeUpdate(zl,p+reqlen);
p = zl+offset;
}
// 一切搞定,将前置节点的长度写入新节点的 header
p += zipPrevEncodeLength(p,prevlen);
// 将节点值的长度写入新节点的 header
p += zipEncodeLength(p,encoding,slen);
// 写入节点值
if (ZIP_IS_STR(encoding)) {
// T = O(N)
memcpy(p,s,slen);
} else {
// T = O(1)
zipSaveInteger(p,value,encoding);
}
// 更新列表的节点数量计数器
// T = O(1)
ZIPLIST_INCR_LENGTH(zl,1);
return zl;
}
delete方法
下面图展示了这个zipList的删除方法的主要的操作,下面的灰色部分为要删除的节点,首先更新需要删除节点的后续节点的preLength以及ztail的数据,更新完以后将要删除节点的节点的内存向前移动,然后进行内存重新设置,清除多余的内存,当然最后还是会级联更新后续节点的preLength数值。
static unsigned char *__ziplistDelete(unsigned char *zl, unsigned char *p, unsigned int num) {
unsigned int i, totlen, deleted = 0;
size_t offset;
int nextdiff = 0;
zlentry first, tail;
// 计算被删除节点总共占用的内存字节数
// 以及被删除节点的总个数
// T = O(N)
first = zipEntry(p);
for (i = 0; p[0] != ZIP_END && i < num; i++) {
p += zipRawEntryLength(p);
deleted++;
}
// totlen 是所有被删除节点总共占用的内存字节数
totlen = p-first.p;
if (totlen > 0) {
if (p[0] != ZIP_END) {
// 执行这里,表示被删除节点之后仍然有节点存在
// 因为位于被删除范围之后的第一个节点的 header 部分的大小
// 可能容纳不了新的前置节点,所以需要计算新旧前置节点之间的字节数差
// T = O(1)
nextdiff = zipPrevLenByteDiff(p,first.prevrawlen);
// 如果有需要的话,将指针 p 后退 nextdiff 字节,为新 header 空出空间
p -= nextdiff;
// 将 first 的前置节点的长度编码至 p 中
// T = O(1)
zipPrevEncodeLength(p,first.prevrawlen);
/* Update offset for tail */
// 更新到达表尾的偏移量
// T = O(1)
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))-totlen);
// 如果被删除节点之后,有多于一个节点
// 那么程序需要将 nextdiff 记录的字节数也计算到表尾偏移量中
// 这样才能让表尾偏移量正确对齐表尾节点
// T = O(1)
tail = zipEntry(p);
if (p[tail.headersize+tail.len] != ZIP_END) {
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+nextdiff);
}
// 从表尾向表头移动数据,覆盖被删除节点的数据
// T = O(N)
memmove(first.p,p,
intrev32ifbe(ZIPLIST_BYTES(zl))-(p-zl)-1);
} else {
// 执行这里,表示被删除节点之后已经没有其他节点了
// T = O(1)
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe((first.p-zl)-first.prevrawlen);
}
// 缩小并更新 ziplist 的长度
offset = first.p-zl;
zl = ziplistResize(zl, intrev32ifbe(ZIPLIST_BYTES(zl))-totlen+nextdiff);
ZIPLIST_INCR_LENGTH(zl,-deleted);
p = zl+offset;
// 如果 p 所指向的节点的大小已经变更,那么进行级联更新
// 检查 p 之后的所有节点是否符合 ziplist 的编码要求
// T = O(N^2)
if (nextdiff != 0)
zl = __ziplistCascadeUpdate(zl,p);
}
return zl;
}