压缩列表(ziplist)是哈希键的底层实现之一。它是经过特殊编码的双向链表,和整数集合(intset)一样,是为了提高内存的存储效率而设计的。
Redis 压缩列表(ziplist)
ziplist数据结构
redis中ziplist是由ziplist header 、entries、zlend三个部分组成,在内存中的布局如下所示:
头部结构
由zlbytes、zltail、zlen三个部分组成:
zlbytes:压缩列表总字节数
zltail:压缩列表头尾偏移量
zlen:压缩列表节点数量
// 10个字节
#define ZIPLIST_HEADER_SIZE (sizeof(uint32_t)*2+sizeof(uint16_t))
entries结构
prev_entry_length:编码前置节点的长度,用于从后往前遍历
encoding:编码属性
contents:负责保存节点的值
prev_entry_length:
ziplist在编码前置节点长度的时候,采用以下规则:
如果前置节点的长度小于254字节,那么采用1个字节来保存这个长度值
如果前置节点的长度大于254字节,则采用5个字节来保存这个长度值,其中,第一个字节被设置为0xFE(254),用于表示该长度大于254字节,后面四个字节则用来存储前置节点的长度值。
encoding:
ziplist的节点可以保存字符串值和整数值
节点保存字符串值
1.如果节点保存的是字符串值,那么该编码大小可能为1字节,2字节或5字节,这与字符串的长度有关。编码部分前两位为00,01或者10,分别对应上述的三种大小,后面的位表示长度大小值。
2.字符串大小编码长度编码<= 63 bytes1 bytes00bbbbbb<= 16383 bytes2 bytes01bbbbbb xxxxxxxx<= 4294967295 bytes5 bytes10________ aaaaaaaa bbbbbbbb cccccccc dddddddd
节点保存整数值
1.如果节点保存的是整数值,那么其编码长度固定为1个字节,该字节的前两位固定为11,用于表示节点保存的是整数值。这里也用一个表来说明。
2.整数类型编码长度编码:
int16_t(2 bytes)111000000
int32_t(4 bytes)111010000
int64_t(8bytes) 111100000
24位有符整数 111110000
8位有符整数 1111111100~1211111xxxx
encoding与计算encoding类型->
//字符串
#define ZIP_STR_06B (0 << 6)
#define ZIP_STR_14B (1 << 6)
#define ZIP_STR_32B (2 << 6)
//整型
#define ZIP_INT_16B (0xc0 | 0<<4)
#define ZIP_INT_32B (0xc0 | 1<<4)
#define ZIP_INT_64B (0xc0 | 2<<4)
#define ZIP_INT_24B (0xc0 | 3<<4)
创建节点
/* Create a new empty ziplist. */
unsigned char *ziplistNew(void) {
unsigned int bytes = ZIPLIST_HEADER_SIZE+1;
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;
}
插入节点
static unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {
// 当前长度和插入节点后需要的长度
size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), reqlen;
// 前置节点长度和编码该长度值所需的长度
unsigned int prevlensize, prevlen = 0;
size_t offset;
int nextdiff = 0;
unsigned char encoding = 0;
//初始化,防止报错
PORT_LONGLONG value = 123456789;
zlentry tail;
// 找出待插入节点的前置节点长度
// 如果p[0]不指向列表末端,说明列表非空,并且p指向其中一个节点
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 = zipRawEntryLength(ptail);
}
}
//判断是否能够编码为整数
if (zipTryEncoding(s,slen,&value,&encoding)) {
//编码为整数的长度
reqlen = zipIntSize(encoding);
} else {
//编码为字符串的长度
reqlen = slen;
}
//获取前置节点的长度
reqlen += zipPrevEncodeLength(NULL,(unsigned int)prevlen); WIN_PORT_FIX /* cast (unsigned int) */
//当前节点的长度
reqlen += zipEncodeLength(NULL,encoding,slen);
/* 只要不是插入到列表的末端,都需要判断当前p所指向的节点header是否能存放新节点的长度编码
* nextdiff保存新旧编码之间的字节大小差,如果这个值大于0
* 那就说明当前p指向的节点的header进行扩展*/
nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,(unsigned int)reqlen) : 0; WIN_PORT_FIX /* cast (unsigned int) */
/* 存储p相遇对列表zl的偏移地址. */
offset = p-zl;
/* 重新分配空间,curlen当前列表的长度
* reqlen 新节点的全部长度
* nextdiff 新节点的后继节点扩展header的长度*/
zl = ziplistResize(zl,(unsigned int)(curlen+reqlen+nextdiff)); WIN_PORT_FIX /* cast (unsigned int) */
// 重新获取p的值
p = zl+offset;
// 非表尾插入,需要重新计算表尾的偏移量
if (p[0] != ZIP_END) {
// 移动现有元素,为新元素的插入提供空间
memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff);
// p+reqlen为新节点前置节点移动后的位置,将新节点的长度编码至前置节点
zipPrevEncodeLength(p+reqlen,(unsigned int)reqlen); WIN_PORT_FIX /* cast (unsigned int) */
// 更新列表尾相对于表头的偏移量,将新节点的长度算上
ZIPLIST_TAIL_OFFSET(zl) =
(uint32_t)intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+reqlen); WIN_PORT_FIX /* cast (uint32_t) */
// 如果新节点后面有多个节点,那么表尾的偏移量需要算上nextdiff的值
zipEntry(p+reqlen, &tail);
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) = (uint32_t)intrev32ifbe(p-zl); WIN_PORT_FIX /* cast (uint32_) */
}
// 当nextdiff不为0时,表示需要新节点的后继节点对头部进行扩展
if (nextdiff != 0) {
offset = p-zl;
// 需要对p所指向的机电header进行扩展更新
// 有可能会引起连锁更新
zl = __ziplistCascadeUpdate(zl,p+reqlen);
p = zl+offset;
}
// 将新节点前置节点的长度写入新节点的header
p += zipPrevEncodeLength(p,(unsigned int)prevlen); WIN_PORT_FIX /* cast (unsigned int) */
// 将新节点的值长度写入新节点的header
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;
}
插入节点的时候有可能会产生连锁更新,我们来看下连锁更新的定义:
当新节点插入后,需要改变新节点后继节点的header信息中的保存前置节点长度的部分,如果这个pre_entry_length原存放的长度小于254字节,也就是只用了一个字节,现在新节点的长度大于254字节,需要用5个字节保存,这样就要对这个pre_entry_length进行扩展。试想一下,如果扩展之后该节点的整体长度大于254字节了,那么该节点的后继节点是不是也需要更新header信息呢?答案是肯定的,这样就引发了连锁更新,导致新节点后面的一连串节点都需要对header进行扩容。
static unsigned char *__ziplistCascadeUpdate(unsigned char *zl, unsigned char *p) {
size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), rawlen, rawlensize;
size_t offset, noffset, extra;
unsigned char *np;
zlentry cur, next;
while (p[0] != ZIP_END) {
// 将p所指向节点的信息保存到cur结构体中
zipEntry(p, &cur);
// 当前节点的长度
rawlen = cur.headersize + cur.len;
// 编码当前节点的长度所需的字节数
rawlensize = zipPrevEncodeLength(NULL,(unsigned int)rawlen); WIN_PORT_FIX /* cast (unsigned int) */
// 如果没有后续节点需要更新了,就退出
if (p[rawlen] == ZIP_END) break;
// 去除后续节点的信息保存到next结构体中
zipEntry(p+rawlen, &next);
// 当后续节点的空间已经足够了,就直接退出
if (next.prevrawlen == rawlen) break;
// 当后续节点的空间不足够,则需要进行扩容操作
if (next.prevrawlensize < rawlensize) {
// 记录p的偏移值
offset = p-zl;
// 记录需要增加的长度
extra = rawlensize-next.prevrawlensize;
// 扩展zl的大小
zl = ziplistResize(zl,(unsigned int)(curlen+extra)); WIN_PORT_FIX /* cast (unsigned int) */
// 获取p相对于新的zl的值
p = zl+offset;
//记录下一个节点的偏移量
np = p+rawlen;
noffset = np-zl;
// 当 next 节点不是表尾节点时,更新列表到表尾节点的偏移量
if ((zl+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))) != np) {
ZIPLIST_TAIL_OFFSET(zl) =
(uint32_t)intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+extra); WIN_PORT_FIX /* cast (uint32_t) */
}
// 向后移动cur节点之后的数据,为新的header腾出空间
memmove(np+rawlensize,
np+next.prevrawlensize,
curlen-noffset-next.prevrawlensize-1);
zipPrevEncodeLength(np,(unsigned int)rawlen); WIN_PORT_FIX /* cast (unsigned int) */
// 移动指针,继续处理下一个节点
p += rawlen;
curlen += extra;
} else {
if (next.prevrawlensize > rawlensize) {
// 执行到这里,next节点编码前置节点的header空间有5个字节
// 但是此时只需要一个字节
// Redis不提供缩小操作,而是直接将长度强制性写入五个字节中
zipPrevEncodeLengthForceLarge(p+rawlen,(unsigned int)rawlen);
} else {
// 运行到这里,说明刚好可以存放
zipPrevEncodeLength(p+rawlen,(unsigned int)rawlen);
}
// 退出,代表空间足够,后续空间不需要更改
break;
}
}
return zl;
}
删除节点
/* Delete "num" entries, starting at "p". Returns pointer to the ziplist. */
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;
// 获取p指向的节点信息
zipEntry(p, &first);
// 计算num个节点占用的内存
for (i = 0; p[0] != ZIP_END && i < num; i++) {
p += zipRawEntryLength(p);
deleted++;
}
totlen = (unsigned int)(p-first.p); WIN_PORT_FIX /* cast (unsigned int) */
if (totlen > 0) {
if (p[0] != ZIP_END) {
// 执行到这里,表示被删除节点后面还存在节点
// 判断最后一个被删除的节点的后继节点的header中的存放前置节点长度的空间
// 能不能容纳第一个被删除节点的前置节点的长度
nextdiff = zipPrevLenByteDiff(p,first.prevrawlen);
p -= nextdiff;
zipPrevEncodeLength(p,first.prevrawlen);
// 更新尾部相对于头部的偏移量
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))-totlen);
// 如果被删除节点后面还存在节点,就需要将nextdiff计算在内
zipEntry(p, &tail);
if (p[tail.headersize+tail.len] != ZIP_END) {
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+nextdiff);
}
// 将被删除节点后面的内存空间移动到删除的节点之后
memmove(first.p,p,
intrev32ifbe(ZIPLIST_BYTES(zl))-(p-zl)-1);
} else {
// 执行到这里,表示被删除节点后面没有节点了
ZIPLIST_TAIL_OFFSET(zl) =
(unsigned int)intrev32ifbe((first.p-zl)-first.prevrawlen); WIN_PORT_FIX /* cast (unsigned int) */
}
// 缩小内存并更新ziplist的长度
offset = first.p-zl;
zl = ziplistResize(zl, intrev32ifbe(ZIPLIST_BYTES(zl))-totlen+nextdiff);
ZIPLIST_INCR_LENGTH(zl,-deleted);
p = zl+offset;
// 如果nextdiff不等于0,说明被删除节点后面节点的header信息还需要更改,需要进行连锁更新
if (nextdiff != 0)
zl = __ziplistCascadeUpdate(zl,p);
}
return zl;
}
end结构
ziplist尾部的zlend则表示压缩列表结束,其值固定为0xFF,长度为1个字节。