redis中的list是有多种实现的,其中一种是ziplist,其介绍如下
ziplist 是一个经过特殊编码的双向链表,旨在提高内存效率。 它存储字符串和整数值,其中整数被编码为实际整数而不是一系列字符。 它允许在 O(1) 时间内在列表的任一侧进行推送和弹出操作。 但是,由于每个操作都需要重新分配 ziplist 使用的内存,因此实际复杂性与 ziplist 使用的内存量有关。
ziplist是一个双向链表结构,是一整块紧凑的内存块,当大小不足需要重新扩展,底层使用je_realloc
进行扩展。
typedef struct zlentry {
unsigned int prevrawlensize; // prevrawlen字段的字节数大小,前一节点的大小的类型,1字节或者5字节
unsigned int prevrawlen; // 前一个节点的的长度
unsigned int lensize; // len字段的字节数大小
unsigned int len;
unsigned int headersize; /* prevrawlensize + lensize. */
unsigned char encoding; /* Set to ZIP_STR_* or ZIP_INT_* depending on
the entry encoding. However for 4 bits
immediate integers this can assume a range
of values and must be range-checked. */
unsigned char *p; /* Pointer to the very start of the entry, that
is, this points to prev-entry-len field. */
} zlentry;
zipList中在实际计算的时候,使用zlentry来表示每个节点,其中的prevrawlen
是前一个节点的大小,len
表示的是当前节点的大小,由于整个ziplist是一整块的内存块,通过prevrawlen
和len
字段,我们就能够向前或向后便利整个链表。
需要注意的是,ziplist中的节点并不是一个zlentry,而是在计算的时候使用zlentry来模拟,实际ziplist中的每个节点,大概是如下结构``: ![在这里插入图片描述](https://img-blog.csdnimg.cn/cb20b5e505d841e1b12fb0aa9015e587.png) 通过
zipEntry函数,将ziplist中一个entry节点信息,填充到一个
zlentry中,这二者只是共享了同一个实际数据
p`的指针。
static inline void zipEntry(unsigned char *p, zlentry *e) {
ZIP_DECODE_PREVLEN(p, e->prevrawlensize, e->prevrawlen);
ZIP_ENTRY_ENCODING(p + e->prevrawlensize, e->encoding);
ZIP_DECODE_LENGTH(p + e->prevrawlensize, e->encoding, e->lensize, e->len);
assert(e->lensize != 0); /* check that encoding was valid. */
e->headersize = e->prevrawlensize + e->lensize;
e->p = p;
}
整个ziplist的结构如上:zlbytes表示的是整个ziplist所占用的空间大小,zltail是尾节点的位置,zllen表示的是当前有多少个节点,zlend是一个标志位,表示的是链表的结尾。一个byte,为255.
在创建ziplist的时候,初始的时候只创建了一个空的ziplist表头:
/* Create a new empty ziplist. */
//创建一个空的ziplist
unsigned char *ziplistNew(void) {
// 这里只分配了表头,即 zlbytes,zltail,zllen三个字段记录list的相关信息
// zlbytes记录整个list的占用空间,zltail记录整个list中最后一个entry的偏移量,zllen记录entry的数量
unsigned int bytes = ZIPLIST_HEADER_SIZE+ZIPLIST_END_SIZE;
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;
return zl;
}
初始分配了一个空的ziplist,只分配了zlbytes、zltai、zllen、,zlend这四个字段。当我们要插入一个节点时,通过ziplistPush
方法插入
unsigned char *ziplistPush(unsigned char *zl, unsigned char *s, unsigned int slen, int where) {
unsigned char *p;
p = (where == ZIPLIST_HEAD) ? ZIPLIST_ENTRY_HEAD(zl) : ZIPLIST_ENTRY_END(zl);
return __ziplistInsert(zl,p,s,slen);
}
unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int 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. Using a value
that is easy to see if for some reason
we use it uninitialized. */
zlentry tail;
/* Find out prevlen for the entry that is inserted. */
if (p[0] != ZIP_END) {
ZIP_DECODE_PREVLEN(p, prevlensize, prevlen);
} else {
unsigned char *ptail = ZIPLIST_ENTRY_TAIL(zl);
if (ptail[0] != ZIP_END) {
prevlen = zipRawEntryLengthSafe(zl, curlen, ptail);
}
}
/* See if the entry can be encoded */
if (zipTryEncoding(s,slen,&value,&encoding)) {
/* 'encoding' is set to the appropriate integer encoding */
reqlen = zipIntSize(encoding);
} else {
/* 'encoding' is untouched, however zipStoreEntryEncoding 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 += zipStorePrevEntryLength(NULL,prevlen);
reqlen += zipStoreEntryEncoding(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. */
int forcelarge = 0;
nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0;
if (nextdiff == -4 && reqlen < 4) {
nextdiff = 0;
forcelarge = 1;
}
/* Store offset because a realloc may change the address of zl. */
offset = p-zl;
newlen = curlen+reqlen+nextdiff;
zl = ziplistResize(zl,newlen);
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. */
if (forcelarge)
zipStorePrevEntryLengthLarge(p+reqlen,reqlen);
else
zipStorePrevEntryLength(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. */
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 {
/* 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 += 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;
}
我们来分析下这个过程:
- 判断当前插入是在头结点插入还是尾结点插入,如果是头结点插入,那么当前指针偏移如下位置
#define ZIPLIST_HEADER_SIZE (sizeof(uint32_t)*2+sizeof(uint16_t))
结合上面的结构图,这个应该很明显。如果是尾结点,则偏移#define ZIPLIST_ENTRY_END(zl) ((zl)+intrev32ifbe(ZIPLIST_BYTES(zl))-1)
,这里的-1 是插入放在zlend前面。这样就拿到了待插入的节点在链表中的具体位置 - 拿到插入的位置后,通过
__ziplistInsert
进行实际的插入操作。在这里,首先会判断待插入的数据是否全部是数字还是是字符串。如果是数字的话,则会将字符串转换为数字保存,这样能够减少内存空间(并且redis在这里还会判断数字是在哪个范围,继而使用uint_8、uint_16、uint_32、uint_64),如果不是数字的话,则按照实际字符串的长度申请大小。 - 这里是节点本身内容的大小,还有节点需要额外记录前驱节点信息,
prevlen
和encoding
信息 - 对整个链表扩容,并移动数据,移动数据主要是ZIP_END节点,
- 写入preLen和encoding信息
- 复制数据内容到当前节点
这样我们就在当前list上插入了一个节点。
接下来我们看看,通过一个index获取list中的一个节点ziplistIndex
:
unsigned char *ziplistIndex(unsigned char *zl, int index) {
unsigned char *p;
unsigned int prevlensize, prevlen = 0;
size_t zlbytes = intrev32ifbe(ZIPLIST_BYTES(zl));
p = ZIPLIST_ENTRY_HEAD(zl);
while (index--) {
/* Use the "safe" length: When we go forward, we need to be careful
* not to decode an entry header if it's past the ziplist allocation. */
p += zipRawEntryLengthSafe(zl, zlbytes, p);
if (p[0] == ZIP_END)
break;
}
if (p[0] == ZIP_END || index > 0)
return NULL;
zipAssertValidEntry(zl, zlbytes, p);
return p;
}
可以看到,这里就是通过不断移动指针,来到达指定节点指针,比如我们获取index = 6位置的节点,那么进行5次指针偏移计算,从而到达第6个节点指针。