redis中的压缩列表在插入数据的时候可能存在连锁扩容的情况。
在压缩列表中,节点需要存放上一个节点的长度,当上一个entry节点长度小于254个字节的时候,将会一个字节的大小来存放entry中的数据,但是当其长度大于等于254的时候,就会需要更大的空间来存放数据。在压缩列表中,会把大于等于254字节长度用5个字节来存储,第一个字节是254,当读到254的时候,将会确认接下来的4个字节大小将是entry的长度数据。当第一个字节为255的时候,就证明压缩列表已经到达末端。
由于表示长度的字节大小不一样,当新节点的插入可能会导致下一个节点原本存放表示上一节点的长度的空间大小不够导致需要扩容这一字段。相应的该字段将会由一个字节扩容到五个字节,四个字节的长度变化,当发生变化的节点原本长度在250到253之间的时候,将会导致下一个节点存储上节点长度的空间发生变化,引起一个连锁扩容的情况,这一情况将会直到一个不需要扩容的节点为止。
while (p[0] != ZIP_END) {
zipEntry(p, &cur);
rawlen = cur.headersize + cur.len;
rawlensize = zipStorePrevEntryLength(NULL,rawlen);
/* Abort if there is no next entry. */
if (p[rawlen] == ZIP_END) break;
zipEntry(p+rawlen, &next);
/* Abort when "prevlen" has not changed. */
if (next.prevrawlen == rawlen) break;
if (next.prevrawlensize < rawlensize) {
/* The "prevlen" field of "next" needs more bytes to hold
* the raw length of "cur". */
offset = p-zl;
extra = rawlensize-next.prevrawlensize;
zl = ziplistResize(zl,curlen+extra);
p = zl+offset;
/* Current pointer and offset for next element. */
np = p+rawlen;
noffset = np-zl;
/* Update tail offset when next element is not the tail element. */
if ((zl+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))) != np) {
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+extra);
}
/* Move the tail to the back. */
memmove(np+rawlensize,
np+next.prevrawlensize,
curlen-noffset-next.prevrawlensize-1);
zipStorePrevEntryLength(np,rawlen);
/* Advance the cursor */
p += rawlen;
curlen += extra;
} else {
if (next.prevrawlensize > rawlensize) {
/* This would result in shrinking, which we want to avoid.
* So, set "rawlen" in the available bytes. */
zipStorePrevEntryLengthLarge(p+rawlen,rawlen);
} else {
zipStorePrevEntryLength(p+rawlen,rawlen);
}
/* Stop here, as the raw length of "next" has not changed. */
break;
}
}
上面这段代码就是执行连锁扩容逻辑的代码。
首先,从新插入的节点的下一个节点开始,如果下一个节点存放上一个字节的空间大小大于或等于当前的节点长度,那么在存放了这一长度数据之后,该次连锁扩容直接宣告结束。
如果下一个节点存放长度的空间不能容纳当前节点的长度,那么就会将下一个节点进行扩容,并重新申请内存大小,并复制数据,移动指向尾部节点的指针。最后移动到下一个节点,在下一个循环中判断是否需要继续扩容。