redis数据结构之压缩列表

压缩列表用于存储长度受限的字符串和整数。废话不多说,直接上redis压缩列表的内存结构示意图:


从图中可以看出,redis压缩列表由表示压缩列表占总内存的字节数的zlbytes,表示到达ziplist 表尾节点的偏移量的zltail,表示ziplist 中节点的数量的zllen,各个节点以及用于标记ziplist的末端的zlend。

注意:zllen并不是一直表示节点的数量,只有zllen小于UINT16_MAX时才是,当这个值等于UINT16_MAX时,节点的数量需要遍历整个ziplist 才能计算得出。zlend的值是固定的,也就是255.

从图中可以看出,压缩列表总是有11个字节的固定长度(4+4+2+1),而这11个字节的长度也就是压缩列表的头部与尾部,在新建一个压缩列表的时候,也就是只有这11个字节,下面来看下新建压缩列表的函数:

unsigned char *ziplistNew(void) {
    // 分配 2 个 32 bit,一个 16 bit,以及一个 8 bit
    // 分别用于 <zlbytes><zltail><zllen> 和 <zlend>
    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;
}
而ZIPLIST_HEADER_SIZE的定义是个宏,如下:
#define ZIPLIST_HEADER_SIZE     (sizeof(uint32_t)*2+sizeof(uint16_t)) 
结合上面的宏可以看出ziplistNew中的bytes就是11。ziplistNew的功能也很简单就不多说了。

在讲述压缩列表插入之前,先要介绍下其余的东西。上面的图讲述了压缩列表的格式,但是并没有各个节点的格式,下面以一张图来描述下:

上图就描述了各个节点的格式,但是节点中域的长度并不是固定的,下面来讲述下:

pre_entry_length从字面意思就能看出来它表示前一个节点的长度,pre_entry_length可以占用1个字节也可以占用5个字节。如果前一节点的长度小于254 字节,那么只使用一个字节保存它的值。如果前一节点的长度大于等于254 字节,那么将第1 个字节的值设为254 ,然后用接下来的4 个字节保存实际长度。

encoding可以分为四种,其中三种是为字符串准备的,最后一种就是为整数准备的。具体如下表格所示:


以上是编码字符串所用到的。下面的是编码整数的


下面结合插入数据到压缩表来讲述这些编码方式。插入数据到压缩表主要是通过ziplistInsert,这个函数会调用__ziplistInsert,而实际插入数据的也是__ziplistInsert这个函数来进行操作的。这里我只列出比较重要的一部分代码。

static unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {
.....
if (zipTryEncoding(s,slen,&value,&encoding)) {
        /* 'encoding' is set to the appropriate integer encoding */
        // s 可以保存为整数,那么继续计算保存它所需的空间
        reqlen = zipIntSize(encoding);
    } else {
        /* 'encoding' is untouched, however zipEncodeLength will use the
         * string length to figure out how to encode it. */
        // 不能保存为整数,直接使用字符串长度
        reqlen = slen;
    }

    // 计算编码 prevlen 所需的长度
    reqlen += zipPrevEncodeLength(NULL,prevlen);
    // 计算编码 slen 所需的长度
    reqlen += zipEncodeLength(NULL,encoding,slen);

// 如果添加的位置不是表尾,那么必须确定后继节点的 prevlen 空间
    // 足以保存新节点的编码长度
    // zipPrevLenByteDiff 的返回值有三种可能:
    // 1)新旧两个节点的编码长度相等,返回 0
    // 2)新节点编码长度 > 旧节点编码长度,返回 5 - 1 = 4
    // 3)旧节点编码长度 > 新编码节点长度,返回 1 - 5 = -4
    nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0;

.....

// 如果新节点不是添加到列表末端,那么它后面就有其他节点
    // 因此,我们需要移动这部分节点
    if (p[0] != ZIP_END) {
        /* Subtract one because of the ZIP_END bytes */
        // 向右移动移原有数据,为新节点让出空间
        // O(N)
        memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff);

        /* Encode this entry's raw length in the next entry. */
        // 将本节点的长度编码至下一节点
        zipPrevEncodeLength(p+reqlen,reqlen);

        /* Update offset for tail */
        // 更新 ziplist 的表尾偏移量
        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. */
        // 有需要的话,将 nextdiff 也加上到 zltail 上
        tail = zipEntry(p+reqlen);
        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 的 zltail 属性,现在新添加节点为表尾节点
        ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(p-zl);
    }

.....

if (nextdiff != 0) {
        offset = p-zl;
        // O(N^2)
        zl = __ziplistCascadeUpdate(zl,p+reqlen);
        p = zl+offset;
    }
......
}
代码中大部分都有注释,也比较简单,这里我要说的是if(nextdiff!=0)的情况,如果nextdiff不为0,说明新插入的数据的长度与以前这个位置的数据长度不同,而next中的pre_entry_length就需要进行改变,所以要扩展或者收缩next的大小,next大小的改变也同时需要改变next下一个节点的pre_entry_length,直到整个压缩列表全部进行更改。而上面的if语句就是做这件事的。


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值