redis中ziplist

ziplist是一个压缩的双向列表。传统的双向链表,在每个节点,都需要指向下一个和前一个节点的指针,占据了一定的空间;同时双向链表中使用字符串保存了节点的值,对于整形的数值而言,比较费空间。ziplist在这些方面进行了一些优化。    下面跟着源码来学习下:      结构        其中zlbytes 整个列表所占据的空间。

大家好,又见面了,我是你们的朋友全栈君。

        ziplist 是一个压缩的双向列表。传统的双向链表,在每个节点,都需要指向下一个和前一个节点的指针,占据了一定的空间;同时双向链表中使用字符串保存了节点的值,对于整形的数值而言,比较费空间。ziplist 在这些方面进行了一些优化。

       下面跟着源码来学习下:

 

         结构      <zlbytes><zltail><zllen><entry><entry><zlend>

         其中 zlbytes   整个列表所占据的空间。

                  zltail  最后一个节点的下标,这个是便于从后往前遍历。

                  zllen  列表中的节点个数

                  entry  节点

                  zlend 结束标识符号

        每个节点的结构如下  <pre_node_len> <node_encode><node>

         其中pre_node_len表示前面一个节点占据的空间,这样可以从后面往前面遍历节点

                 node_encode编码以及数据信心, 具体编码如下:

1,      00pppppp   前面两个bit 是00,那么表示长度小于64的字符串,后面剩下的6个bit表示字符串的长度[0-63]
2,      01pppppp|qqqqqqqq 前面两个bit是01,那么整个信息占两个字节,剩下的14个字节来表示字符串的长度[64 – 2^14-1]

3,     10______|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt|  前面两个bit是10,那么整个信息占5个字节,剩下的4个字节来表示字符串长度
4,  1100____  表示节点是一个整数,并且能用两个字节表示的
5,  1101____  表示节点是一个整数,并且能用四个字节表示的
6,  1110____  表示节点是一个整数,并且能用八个字节表示的

          通过上面的介绍,我们举个简单的例子,比如一个小于64位字符串(前面节点长度小于254),那么需要     1 + 1 = 2 个字节存储额外信息(非内容)

下图各种编码需要占据的空间(byte)

编码方式 前面节点长度小于254 大于254            
1      1+ 1 = 2  1 + 5 = 6
2      2+ 1 = 3   2 + 5 = 7
3      5+ 1  =6   5+ 5 = 10
4      1 + 1 = 2   1 + 5 = 6
5      1 + 1 = 2    1 + 5=6
6      1 + 1 = 2    1 + 5 = 6

从上图可以看出,大部分情况,比使用链表消耗的8个byte(4个pre指针,4个next指针) 省。

但是也是需要代价的,实现复杂,特别插入和删除过程,需要内存的移动。

看下插入过程:

/* Insert item at "p". */
static unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {
    size_t curlen = ZIPLIST_BYTES(zl), reqlen, prevlen = 0;
    size_t offset;
    int nextdiff = 0;
    unsigned char encoding = 0;
    long long value;
    zlentry entry, tail;

    //计算插入节点前面一个节点的长度: 1 如果不是插入最后面,那么直接从原来的节点可以获取,2 如果是最后插入,那么就要计算末个节点的长度
    /* Find out prevlen for the entry that is inserted. */
    if (p[0] != ZIP_END) {
        entry = zipEntry(p);
        prevlen = entry.prevrawlen;
    } else {
        unsigned char *ptail = ZIPLIST_ENTRY_TAIL(zl);
        if (ptail[0] != ZIP_END) {
            prevlen = zipRawEntryLength(ptail);
        }
    }

    //计算需要占据的空间 
    /* See if the entry can be encoded */
    if (zipTryEncoding(s,slen,&value,&encoding)) {
        /* 'encoding' is set to the appropriate integer encoding */
        reqlen = zipIntSize(encoding);
    } else {
        /* 'encoding' is untouched, however zipEncodeLength will use the
         * string length to figure out how to encode it. */
        reqlen = slen;
    }
    //计算整个节点需要占据的空间(pre_len, slen, content)
    /* We need space for both the length of the previous entry and
     * the length of the payload. */
    reqlen += zipPrevEncodeLength(NULL,prevlen);
    reqlen += zipEncodeLength(NULL,encoding,slen);
    //不过不是末尾插入,需要考虑下个节点纪录当前节点的长度的空间是否够
    /* When the insert position is not equal to the tail, we need to
     * make sure that the next entry can hold this entry's length in
     * its prevlen field. */
    nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0;

    /* Store offset because a realloc may change the address of zl. */
    offset = p-zl;
    zl = ziplistResize(zl,curlen+reqlen+nextdiff);
    p = zl+offset;

    /* Apply memory move when necessary and update tail offset. */
    if (p[0] != ZIP_END) {
        /* Subtract one because of the ZIP_END bytes */
        // (p-nexdiff )  --> (p+reqlen)
        memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff);

        /* Encode this entry's raw length in the next entry. */
        zipPrevEncodeLength(p+reqlen,reqlen);

        //如果下个节点就是最后节点,那么结尾节点下标移动reqlen,否者把next diff 考虑进去
        /* Update offset for tail */
        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. */
        tail = zipEntry(p+reqlen);
        if (p[reqlen+tail.headersize+tail.len] != ZIP_END)
            ZIPLIST_TAIL_OFFSET(zl) += nextdiff;
    } else {
        /* This element will be the new tail. */
        ZIPLIST_TAIL_OFFSET(zl) = p-zl;
    }

    //如果下个节点长度变化,那么需要修改下下个节点对应的字段,而这个操作有可能导致下下个节点的长度发生变化,所以需要往##修改,知道某个节点长度不发生改变
    /* When nextdiff != 0, the raw length of the next entry has changed, so
     * we need to cascade the update throughout the ziplist */
    if (nextdiff != 0) {
        offset = p-zl;
        zl = __ziplistCascadeUpdate(zl,p+reqlen);
        p = zl+offset;
    }

    /* Write the entry */
    //写入节点信息
    p += zipPrevEncodeLength(p,prevlen);
    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;
}

需要注意几点 :  1 因为使用连续内存,所以当在中间插入的时候,需要把后面的节点往后移动。

                               2  插入节点,因为下个节点需要保存当前节点的长度,因为纪录这个长度使用压缩算法,所以可能导致下个节点占据的空间发生变化,如果发生变化,那么就需要调整下个节点,这样又会导致下下个节点,所以这里需要做调整。 

删除和插入过程相反,所要考虑的也就是上面两点。 其中第二点单独有个函数来实现:

static unsigned char *__ziplistCascadeUpdate(unsigned char *zl, unsigned char *p) {
    size_t curlen = ZIPLIST_BYTES(zl), rawlen, rawlensize;
    size_t offset, noffset, extra;
    unsigned char *np;
    zlentry cur, next;
    //如果一直有变化,那么一直到结尾
    while (p[0] != ZIP_END) {
        cur = zipEntry(p);
        rawlen = cur.headersize + cur.len;
        rawlensize = zipPrevEncodeLength(NULL,rawlen);

        /* Abort if there is no next entry. */
        if (p[rawlen] == ZIP_END) break;
        next = zipEntry(p+rawlen);

        /* 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+ZIPLIST_TAIL_OFFSET(zl)) != np)
                ZIPLIST_TAIL_OFFSET(zl) += extra;

            /* Move the tail to the back. */
            memmove(np+rawlensize,
                np+next.prevrawlensize,
                curlen-noffset-next.prevrawlensize-1);
            zipPrevEncodeLength(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. */
                //通过浪费4个byte,来避免内存移动
                zipPrevEncodeLengthForceLarge(p+rawlen,rawlen);
            } else {
                zipPrevEncodeLength(p+rawlen,rawlen);
            }

            /* Stop here, as the raw length of "next" has not changed. */
            break;
        }
    }
    return zl;
}

整个过程就是: 从当前位置往后循环,如果节点需要增长,那么就根据增加的大小,移动数据,修改下标等。如果缩小了,这里为避免移动,采用了一个技巧,就是大空间存小数据,浪费4个bye

         

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/127112.html原文链接:https://javaforall.net

赞 (0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • 8款安卓数据恢复软件测评 2019更新版【国外篇】

    8款安卓数据恢复软件测评【国外篇】相信多数人都有过手机数据丢失的悲痛经历吧,尤其是当你没有任何可用的备份的时候。前几天我也遇到了这个问题,那个着急和纠心啊,于是我就开始了全网搜索国内外一切可用的手机数据恢复软件。我把网上能找的能试用的都给试了一遍,这里给大家总结一下!和手机系统一样,现在的手机数据恢复软件也区分为安卓,iOS和WP等,目前市场上主流的是针对安…

    2022年4月9日
  • 6.Page对象详解

    6.Page对象详解序就好像用户看到的都是由dom表现出来的,所有的业务处理都是在Page对象中处理的。如果业务越简单,创建的Page对象数量就会越少;如果业务越复杂,那么相对而言Page对象数量就越多(或Page实例对象就会越复杂)。Page对象主要做以下事情:渲染页面,保证dom元素的合理安排,以及事件的处理;数据交互,将数据正确的放在dom元素中,并进行合理的前后端数据交互。无论是在哪个时候,这两点确实是前端开发的重中之重,换一句话说这就是前端核心开发内容。为了让Page对象更加专注于上面所提的两件事情,

    2022年7月11日
  • 灵格斯:很好很强大的免费电子辞典「建议收藏」

    灵格斯:很好很强大的免费电子辞典「建议收藏」http://www.readfree.net/htm/200807/4624781.html 本文向大家推介近年来出现的国产免费电子辞典软件“灵格斯”(Lingoes),分为四个部分。首先是基本介绍,然后把它和几款同类软件进行了比较,接下来分享我在实用中发现的3个技巧,最后总结了有关的网址链接。本文以主观片面为原则,效果如何,请指教。1.介绍=============

    2022年7月15日
  • client profile_clienttop

    client profile_clienttopscreenX:鼠标在显示屏幕上的坐标。clientX:鼠标在页面显示区域的坐标。注:以上两个都是各浏览器通用的。pageX:FF特有,鼠标在页面上的位置,从页面左上角开始定位,这个可以很方便在整个页面上进行定位,IE没有直接替换的属性。layerX:FF特有,鼠标相对于“触发事件的元素的层级关系中离该元素最近的,设置了position的父元素”的边界的位置,从border的左…

    2022年10月29日
  • linux将目录移动到指定目录下_shell获取文件所在路径

    linux将目录移动到指定目录下_shell获取文件所在路径摘要腾兴网为您分享:第七应用,flash29,信息来电闪光灯,优惠购,实况足球2018,汕大附一,同学聚会简单通知书,cajviewer,惠普deskjet1010,英魂之刃,运动世界,建行大学,爱奇艺会员,sketchup渲染器,电脑尺子,中俄边境游,山东政务服务网等知识经验教程,欢迎大家关注腾兴网。linux系统下,用户根据需求来进行各项操作,有小伙伴想移动文件,操作起来不大容易,linux…

    2022年10月5日
  • cpu后缀讲解

    cpu后缀讲解Intel桌面式CPUX后缀 X代表Extreme,中文意思是至尊级,代表同一时代性能最强的CPU。如Corei7-5960X、Corei7-4960X。X代表在同一代中只有一款CPU黄袍加身,地位至高无上。加上没有竞争对手可以望其项背,从露面都退出市场,期待的弑君者没有出现。SandyBridge时代到现在,竞争的天平一直向Intel倾斜。K后缀自从SandyBridge时代Intel限制

    2022年5月7日

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号