ziplist同intset一样是Redis独有的,主要是为了节约内存,提高存储效率而产生出来的,经过了特殊编码的双向链表。但是与双向链表不同的是,ziplist是一块连续的内存,在这块连续的内存中不同的节点可以是字符串也可以是整数。同时对整数的存储也是使用了变长编码的方式,以此来更进一步的节约内存。存储模式是小端模式。
没有用自定义的struct之类的来表达,而就是简单的unsigned char *。这是因为ziplist本质上就是一块连续内存,内部组成结构又是一个高度动态的设计(变长编码),也没法用一个固定的数据结构来表达。
因为ziplist里面每个数据项开头都有前置节点长度和当前节点长度,所以可以像双端链表那样进行遍历。
虽然说ziplist被当做了数据类型的一种,但是由于它是一块连续的内存,所以可以和sds一样只需要一个char*指针即可表示。而它内部的每一个节点都有自己的构成。
ziplist内存结构如下:
< zlbytes > < zltail > < zllen > < entry 1>< entry2 >… < entryN > < zlend >
< zlbytes >:32bit,表示ziplist所占字节数,是一个无符号数(也包括< zlbytes >本身所占4字节);
< zltail >:32bit,表示ziplist表中最后一项entry在ziplist中的偏移字节数;
< zllen >:16bit,表示ziplist表中数据项的个数,当可表达的个数超过2^16-1时,ziplist仍然可以使用,只是zllen不再有效,此时若想要知道长度就只能通过遍历。
< entry >:真正存放数据的数据项,每一个节点长度视内容而定,每个节点又有自己的格式 :< prevrawlen >< len > < data >
< zlend >:一个字节 1111 1111 代表结束标识符 所以通常在ziplist中表示长度的标志位都不会使用255这个数值。
该结构体在实际ziplist存储中是并不存在的,在实际中每个ziplist中的每个entry的格式都依旧是一段连续的内存保存着< prevrawlen >< len > < data >这样一段数据作为一个节点的。该结构体只是在部分函数API中内部根据内存数据生成的以方便进行各种修改操作。
typedef struct zlentry {
//前置节点长度,和编码该长度用的字节数,可能为1个字节或者5个字节,取决于前面是不是小于254个字节
unsigned int prevrawlensize, prevrawlen;
//当前节点长度和编码该长度用的字节数
unsigned int lensize, len;
//prevrawlensize+lensize 当前节点总字节数
unsigned int headersize;
//本节点编码方式 9种 字符三种00 01 10 数字6种2/4/8/3/1字节和4bit表示的0-12 其中若表示0-12可以直接存储不需要指针
unsigned char encoding;
//指向当前节点的指针,这个指针是开始位置
unsigned char *p;
} zlentry;
前置节点长度< prerawlen >:
这里的prelen可已由一个字节表示也可以由5个字节表示。
- 当前一节点长度小于254时就只需要一个字节8个bit就足够表示,其中不使用255是因为255被当做了结束标志。当一个节点开头是255那么说明该ziplist结束了。
- 当前一节点长度大于等于254时就会使用两个字节来存储prerawlen,第一个字节为254(1111 1110)作为标志,后面4个字节组成一个整型用来存储长度。
当前节点长度< len >:
本字段非常的复杂,本字段既用来标志存储数据的类型也用来表示数据项的长度。总共分为9种情况,其中字符串占三种,数字占6种。
字段开头为00/01/10代表后面数据项为字符串:
00xxxxxx 表明用6个bit位来表示长度,说明数据项长度在[0,2^6-1]之间
01xxxxxx xxxxxxxx 表明用14个bit位来表示长度,说明数据项长度在[26,214-1]之间
11_____ xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx 表明用32bit来表示长度,第一个字节后六位置空不用,说明数据项长度在[214,232-1]之间
字段开头为11代表后面数据项为数字:
1100 0000 --------------- int16_t类型整数
1101 0000 --------------- int32_t类型整数
1110 0000 ----------------- int64_t类型整数
1111 0000 ----------------- 24bit有符号整数
1111 1110 ------------------ 8bit有符号整数
1111 xxxx ------------------ 4bit无符号整数,表示[0,12] 若是该种情况,len编码直接存储了数据不需要额外的数据项。
/* Find out prevlen for the entry that is inserted. */
if (p[0] != ZIP_END) {
ZIP_DECODE_PREVLEN(p, prevlensize, prevlen); //取出p处的前置节点长度值和编码该长度值的大小
} else {
// 如果 p 指向表尾末端,那么程序需要检查列表是否为:
// 1)如果 ptail 也指向 ZIP_END ,那么列表为空;
// 2)如果列表不为空,那么 ptail 将指向列表的最后一个节点。
unsigned char *ptail = ZIPLIST_ENTRY_TAIL(zl);
if (ptail[0] != ZIP_END) {
prevlen = zipRawEntryLength(ptail); //取出表尾节点的长度 进入这里表明表尾节点将成为新节点的前置节点
}
}
/* See if the entry can be encoded */
// 尝试看能否将输入字符串转换为整数,如果成功的话:
// 1)value 将保存转换后的整数值
// 2)encoding 则保存适用于 value 的编码方式
// 无论使用什么编码, reqlen 都保存节点值的长度
if (zipTryEncoding(s,slen,&value,&encoding)) {
/* 'encoding' is set to the appropriate integer encoding */
reqlen = zipIntSize(encoding);
} else {
/* 'encoding' is untouched, however zipEncodeLength will use the
* string length to figure out how to encode it. */
reqlen = slen;
}
/* We need space for both the length of the previous entry and
* the length of the payload. */
reqlen += zipPrevEncodeLength(NULL,prevlen);
reqlen += zipEncodeLength(NULL,encoding,slen);
/* When the insert position is not equal to the tail, we need to
* make sure that the next entry can hold this entry's length in
* its prevlen field. */
// 只要新节点不是被添加到列表末端,
// 那么程序就需要检查看 p 所指向的节点(的 header)能否编码新节点的长度。
// nextdiff 保存了新旧编码之间的字节大小差,如果这个值大于 0
// 那么说明需要对 p 所指向的节点(的 header )进行扩展
nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0;
/* Store offset because a realloc may change the address of zl. */
offset = p-zl;
zl = ziplistResize(zl,curlen+reqlen+nextdiff);
p = zl+offset;
/* Apply memory move when necessary and update tail offset. */
if (p[0] != ZIP_END) {
/* Subtract one because of the ZIP_END bytes */
memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff);
/* Encode this entry's raw length in the next entry. */
zipPrevEncodeLength(p+reqlen,reqlen);
/* Update offset for tail */
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);
}
/* When nextdiff != 0, the raw length of the next entry has changed, so
* we need to cascade the update throughout the ziplist */
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;
}
插入函数解析:
- 这个函数是在指定的位置p插入一段新的数据,待插入数据的地址指针是s,长度为slen。插入后形成一个新的数据项,占据原来p的配置,原来位于p位置的数据项以及后面的所有数据项,需要统一向后移动,给新插入的数据项留出空间。参数p指向的是ziplist中某一个数据项的起始位置,或者在向尾端插入的时候,它指向ziplist的结束标记< zlend >。
- 函数开始先计算出待插入位置前一个数据项的长度prevlen。这个长度要存入新插入的数据项的< prevrawlen >字段。
- 然后计算当前数据项占用的总字节数reqlen,它包含三部分:< prevrawlen > , < len >和真正的数据。其中的数据部分会通过调用zipTryEncoding先来尝试转成整数。
- 由于插入导致的ziplist对于内存的新增需求,除了待插入数据项占用的reqlen之外,还要考虑原来p位置的数据项(现在要排在待插入数据项之后)的< prevrawlen >字段的变化。本来它保存的是前一项的总长度,现在变成了保存当前插入的数据项的总长度。这样它的< prevrawlen >字段本身需要的存储空间也可能发生变化,这个变化可能是变大也可能是变小。这个变化了多少的值nextdiff,是调用zipPrevLenByteDiff计算出来的。如果变大了,nextdiff是正值,否则是负值。
- 现在很容易算出来插入后新的ziplist需要多少字节了,然后调用ziplistResize来重新调整大小。ziplistResize的实现里会调用allocator的zrealloc,它有可能会造成数据拷贝。
- 现在额外的空间有了,接下来就是将原来p位置的数据项以及后面的所有数据都向后挪动,并为它设置新的< prevrawlen >字段。此外,还可能需要调整ziplist的< zltail >字段。
- 最后,组装新的待插入数据项,放在位置p。