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

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字节数结构
小于2541xxxxxxxx
大于或等于254511111110 xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx

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

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

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

encoding占用字节存储结构取值范围
ZIP_INT_XX1字节11110001~111111010~12
ZIP_INT_8B1字节11111110-2^8~2^8-1
ZIP_INT_16B2字节11000000-2^16~2^16-1
ZIP_INT_24B3字节11110000-2^24~2^24-1
ZIP_INT_32B4字节11010000-2^32~2^32-1
ZIP_INT_64B8字节11100000-2^64~2^64-1

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

encoding占用字节存储结构字符串范围length取值
ZIP_STR_06B1字节00XXXXXX长度<64后6位
ZIP_STR_14B2字节01XXXXXX XXXXXXXX长度<16384后14位
ZIP_STR_32B5字节10000000 XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX长度<2^32-132位

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的“连锁更新”现象发生几率较小,但在使用的过程应尽量规避。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值