redis源码分析-ziplist(压缩链表)

来源:互联网 发布:cpu跑分软件 编辑:程序博客网 时间:2024/05/15 03:41
ziplist结构在redis运用非常广泛,是列表、字典等数据类型的底层结构之一。ziplist的优点在于能够一定程度地节约内存。

ziplist构成

ziplist结构由zip_header、zip_entry、zip_end三部分组成。

这里写图片描述

ZIP_HEADER:顾名思义,压缩列表的头部。内部包含ZIP_BYTES、ZIP_TAIL、ZIP_LENGTH属性。
– ZIP_BYTES:ziplist占用的总字节数,uint32_t型;
– ZIP_TAIL:最后一个节点的偏移量,uint32_t型;
– ZIP_LENGTH:ziplist的长度,uint16_t型;
由此,我们可计算出ZIP_HEADER所占用的字节数:4(ZIP_BYTES)+4(ZIP_TAIL)+2(ZIP_LENGTH)=10.

ZIP_ENTRY : ziplist的节点信息, 节点(entry)结构定义位于ziplist.c。

typedef struct zlentry {    //prevrawlensize: 前节点长度所占的字节数    //prevrawlen: 前节点的长度    unsigned int prevrawlensize, prevrawlen;    //lensize: 当前节点的长度所占字节数    //len: 当前节点长度    unsigned int lensize, len;    //entry: header大小    unsigned int headersize;    //当前节点编码    unsigned char encoding;    //指向当前节点的指针    unsigned char *p;} zlentry;

上面各属性的注释已经说明其代表的含义。可以看出zlentry的属性还是比较多的。实际上,ziplist在存储节点信息时,并没有将zlentry数据结构所有属性保存,而是做了简化。

这里写图片描述

prevlen:前节点的长度。

prevlen 字节数 结构 小于254 1 xxxxxxxx 大于或等于254 5 11111110 xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx

encoding & length : 节点的编码和长度。

节点的编码保存在第一个字节,当字节以“11”开头时,表示节点存储的数据为整型,其他情况视为字符串类型。

由于整型的长度是固定的,因此 只需存储encoding信息,length值可根据编码进行计算得出。

encoding 占用字节 存储结构 取值范围 ZIP_INT_XX 1字节 11110001~11111101 0~12 ZIP_INT_8B 1字节 11111110 -2^8~2^8-1 ZIP_INT_16B 2字节 11000000 -2^16~2^16-1 ZIP_INT_24B 3字节 11110000 -2^24~2^24-1 ZIP_INT_32B 4字节 11010000 -2^32~2^32-1 ZIP_INT_64B 8字节 11100000 -2^64~2^64-1

与整型数据相比,字符串的设计相对复杂些。尤其是字符串的长度(length)取值,设计得非常灵活、巧妙。

encoding 占用字节 存储结构 字符串范围 length取值 ZIP_STR_06B 1字节 00XXXXXX 长度<64 后6位 ZIP_STR_14B 2字节 01XXXXXX XXXXXXXX 长度<16384 后14位 ZIP_STR_32B 5字节 10000000 XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX 长度<2^32-1 32位

ZIP_END:ziplist的结束标识,ZIP_END=255。

ziplist核心函数

ziplistNew: 创建一个ziplist

#define ZIPLIST_BYTES(zl)       (*((uint32_t*)(zl)))#define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t))))#define ZIPLIST_LENGTH(zl)      (*((uint16_t*)((zl)+sizeof(uint32_t)*2)))#define ZIPLIST_HEADER_SIZE     (sizeof(uint32_t)*2+sizeof(uint16_t))unsigned char *ziplistNew(void) {    unsigned int bytes = ZIPLIST_HEADER_SIZE+1;    //为zl分配空间    unsigned char *zl = zmalloc(bytes);    //初始化bytes值    ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);    //初始化tail_offset信息    ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);    //初始化zl_length信息    ZIPLIST_LENGTH(zl) = 0;    //将结束标识添加至尾部    zl[bytes-1] = ZIP_END;    return zl;}

__ziplistInsert : 在指定的节点后,新增一个节点。供ziplistPush和ziplistInsert等函数内部调用。

static unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {    ......    //计算s的长度    if (zipTryEncoding(s,slen,&value,&encoding)) {        reqlen = zipIntSize(encoding);    } else {        reqlen = slen;    }    //前节点长度所占用的字节数    //prevlen:p前置节点的长度    reqlen += zipPrevEncodeLength(NULL,prevlen);    //新节点的编码及长度所占用的字节数    reqlen += zipEncodeLength(NULL,encoding,slen);    //计算p前节点与新节点长度所占用的字节数之差    nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0;    //重置ziplist的大小    offset = p-zl;    zl = ziplistResize(zl,curlen+reqlen+nextdiff);    p = zl+offset;    if (p[0] != ZIP_END) {        //为新节点腾出空间,向后整体移动        memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff);        //将新节点的长度编码至后置节点        //p+reqlen:  定位到后置节点        zipPrevEncodeLength(p+reqlen,reqlen);        // 更新到达表尾的偏移量        ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+reqlen);        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) = intrev32ifbe(p-zl);    }    //新旧节点长度所占用的字节数不一致时,则更新后面节点的信息    if (nextdiff != 0) {        offset = p-zl;        zl = __ziplistCascadeUpdate(zl,p+reqlen);        p = zl+offset;    }    //记录前节点的长度    p += zipPrevEncodeLength(p,prevlen);    //记录节点的编码及长度信息    p += zipEncodeLength(p,encoding,slen);    if (ZIP_IS_STR(encoding)) {        //保存字符串数据        memcpy(p,s,slen);    } else {        //保存整型数据        zipSaveInteger(p,value,encoding);    }    //链表长度+1    ZIPLIST_INCR_LENGTH(zl,1);    return zl;}

__ziplistInsert函数的代码较多,下面给出一个简单的流程图:
这里写图片描述

与__ziplistInsert相左,ziplistDelete提供删除节点功能,实现比较简单,这里就不贴代码了。值得注意的是,与新增节点一样,删除节点时也需要更新p之后的节点前置节点长度信息

ziplist还有一个比较特别的函数:ziplistLen,用于计算ziplist的长度,前面有提到ziplist使用2个字节来保存长度信息,当ziplist长度超出2个字节所能表示的范围时,ziplist是怎么存储的?长度又该如何计算呢?

//计算ziplist的长度unsigned int ziplistLen(unsigned char *zl) {    unsigned int len = 0;    //当 ZIPLIST_LENGTH(zl)<65535 时,则返回其值    if (intrev16ifbe(ZIPLIST_LENGTH(zl)) < UINT16_MAX) {        len = intrev16ifbe(ZIPLIST_LENGTH(zl));    } else {        //当ZIPLIST_LENGTH(zl)=65535 时,需要遍历整个链表,计算其长度        unsigned char *p = zl+ZIPLIST_HEADER_SIZE;        while (*p != ZIP_END) {            p += zipRawEntryLength(p);            len++;        }        /* Re-store length if small enough */        //如果长度小于65535时,更新ziplist的ZIPLIST_LENGTH值        if (len < UINT16_MAX) ZIPLIST_LENGTH(zl) = intrev16ifbe(len);    }    return len;}

ziplist还提供了一些方法,由于篇幅关系,这里就不一一介绍。有兴趣的童鞋可以了解下。

方法名 功能 ziplistMerge 合并两个压缩链表 ziplistPush 向表头/尾添加一个节点 ziplistIndex 查找第N个节点 ziplistNext 查找指定节点的后置节点 ziplistPrev 查找指定节点的前置节点 ziplistGet 获取指定节点的信息 ziplistInsert 在指定的节点后新增节点 ziplistDelete 删除节点 ziplistDeleteRange 从指定的下标开始,删除N个节点 ziplistCompare 比较两个节点值 ziplistFind 查找节点 ziplistBlobLen 获取链表所占用字节数

ziplist连锁更新

前面简单地介绍ziplist的节点构成及几个核心函数。我们知道ziplist的insert和delete节点时,系统会自动更新指定节点之后其他节点的前置节点长度信息。
当某个字符串节点的前置节点长度<254时,使用一个字节进行存储,当前置节点长度>=254,则使用5个字节。试想一下,当将一个长度>=254的节点添加到一段连续的“长度在250~253”节点之前时,此时,插入位置之后的所有节点的前置节点长度信息都将面临更新。这就是所谓的“连锁更新”现象。

下面用实例简单说明下。

这里写图片描述

1).当在entry1前插入p1时,entry1前置节点发生变化。由于P1的长度>=254,那么系统需要为entry1分配5个字节用于存储前置节点长度信息。此时, entry1长度由250变为250+5 = 255个字节。

这里写图片描述
2). 由于entry1长度发生变化(250->255),entry2之前使用一个字节存储entry1长度信息,现在无法容纳新的长度数据,需要系统扩充4个字节的容量进行存储。

这里写图片描述
3). 依此类推,直到entryn节点更新完成后停止。

总结

  1. ziplist由header、entry、end三部分组成。支持从表头至表尾(ZIP_HEADER)和表尾至表头(ZIP_TAIL)遍历.

  2. ziplist的长度:当zip_length<65535时,zip_length代表ziplist长度。当zip_length=65535时,需遍历整个ziplist进行计算得出。

  3. 由于ziplist每个节点保留“前节点的长度”信息,当发生insert、delete等操作时,系统会更新指定节点之后的其他节点的“前节点长度”属性。

  4. 虽然ziplist的“连锁更新”现象发生几率较小,但在使用的过程应尽量规避。

0 0