Redis源码(六)——压缩列表

一、压缩列表(ziplist)的定义与实现

  

压缩列表是一种为了节约内存而开发的顺序数据结构,是列表及哈希对象的底层实现之一。当列表中只有少量的列表元素,且列表元素是小整数值或者较短的字符串,Redis就会用压缩列表作为列表对象的底层实现;当哈希对象中只包含少量键值对,而且键值都是小整数或者长度较短的字符串,Redis就用压缩列表作为哈希的实现。


src中的ziplist.c和ziplist.h包含了压缩列表的定义:

/* Create a new empty ziplist.
 *
 * 创建并返回一个新的 ziplist
 *
 * T = O(1)
 */
unsigned char *ziplistNew(void) {
 
    //ZIPLIST_HEADER_SIZE 是 ziplist 表头的大小
    // 1 字节是表末端 ZIP_END 的大小
   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;
 
    returnzl;
}

一个压缩列表由以下部分组成:


1)ZIPLIST_BYTES:记录压缩列表占用的内存字节

2)ZIPLIST_TAIL_OFFSET:记录压缩列表尾节点剧烈表头起始地址的偏移值,这样就可以在O(1)的时间内访问表尾的节点

3)ZIPLIST_LENGTH:记录压缩列表节点的数目,节点结构在下面介绍

4)ZIP_END:表示压缩列表末端,在ziplist.c开头宏定义了此属性,表示255,即0XFF

 

       压缩列表节点结构如下:

/*
 * 保存 ziplist 节点信息的结构
 */
typedefstruct zlentry {
 
    // prevrawlen :前置节点的长度
    // prevrawlensize :编码 prevrawlen 所需的字节大小
    unsigned int prevrawlensize, prevrawlen;
 
    // len :当前节点值的长度
    // lensize :编码 len 所需的字节大小
    unsigned int lensize, len;
 
    // 当前节点 header 的大小
    // 等于 prevrawlensize + lensize
    unsigned int headersize;
 
    // 当前节点值所使用的编码类型
    unsigned char encoding;
 
    // 指向当前节点的指针
    unsigned char *p;
 
}zlentry;


       zlentry中的prevrawlen属性保存着前一个节点的长度,如果要访问前一个节点,可以根据当前节点的地址-prevrawlen即可得到前一节点的地址,当前一节点长度小于ZIP_BIGLEN(默认254字节),那么prevrawlen长度为1字节,否则其长度为5字节:第一字节设置为0XFE(254)已做标识,之后一字节表示真正的长度,逻辑如下:

staticunsigned int zipPrevEncodeLength(unsigned char *p, unsigned int len) {
 
    // 仅返回编码 len 所需的字节数量
    if (p == NULL) {
        return (len < ZIP_BIGLEN) ? 1 :sizeof(len)+1;
 
    // 写入并返回编码 len 所需的字节数量
    } else {
 
        // 1 字节
        if (len < ZIP_BIGLEN) {
            p[0] = len;
            return 1;
 
        // 5 字节
        } else {
            // 添加 5 字节长度标识
            p[0] = ZIP_BIGLEN;
            // 写入编码
            memcpy(p+1,&len,sizeof(len));
            // 如果有必要的话,进行大小端转换
            memrev32ifbe(p+1);
            // 返回编码长度
            return 1+sizeof(len);
        }
    }
}

lensize属性记录了当前节点值的长度;


encoding保存着当前节点的编码方式,可以有字节数组编码方式与整数编码方式,所有的编码定义如下:

/*Different encoding/length possibilities */
/*
 * 字符串编码和整数编码的掩码
 */
#defineZIP_STR_MASK 0xc0
#defineZIP_INT_MASK 0x30
 
/*
 * 字符串编码类型
 */
#defineZIP_STR_06B (0 << 6)
#defineZIP_STR_14B (1 << 6)
#defineZIP_STR_32B (2 << 6)
 
/*
 * 整数编码类型
 */
#defineZIP_INT_16B (0xc0 | 0<<4)
#defineZIP_INT_32B (0xc0 | 1<<4)
#defineZIP_INT_64B (0xc0 | 2<<4)
#defineZIP_INT_24B (0xc0 | 3<<4)
#defineZIP_INT_8B 0xfe


 

二、压缩列表中的重要逻辑


连锁更新


   prevrawlensize属性保存着前一节点的长度,而且其属性的大小与前一节点的长度有关。那么当压缩列表中有多个连续的、长度介于250-253字节之间的节点z1至zn,因为n个节点的长度均小于254,那么prevrawlensize属性的长度只需1字节。那么考虑一种情况:这个时候在z1前面插入一个长度大于254的节点,那么此时z1中prevrawlensize的长度将变成5字节,这就导致z1的长度也超过了254,那么就会影响到节点z2,z3直至zn,这会导致一系列连锁的更新

       Redis将这种连续多次控件扩展的操作称为“连锁更新”(cascade update),代码逻辑如下:

staticunsigned 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;
 
    // T = O(N^2)
    while (p[0] != ZIP_END) {
 
        // 将 p 所指向的节点的信息保存到 cur 结构中
        cur = zipEntry(p);
        // 当前节点的长度
        rawlen = cur.headersize + cur.len;
        // 计算编码当前节点的长度所需的字节数
        // T = O(1)
        rawlensize =zipPrevEncodeLength(NULL,rawlen);
 
        /* Abort if there is no next entry. */
        // 如果已经没有后续空间需要更新了,跳出
        if (p[rawlen] == ZIP_END) break;
 
        // 取出后续节点的信息,保存到 next 结构中
        // T = O(1)
        next = zipEntry(p+rawlen);
 
        /* Abort when "prevlen" hasnot changed. */
        // 后续节点编码当前节点的空间已经足够,无须再进行任何处理,跳出
        // 可以证明,只要遇到一个空间足够的节点,
        // 那么这个节点之后的所有节点的空间都是足够的
        if (next.prevrawlen == rawlen) break;
 
        if (next.prevrawlensize    |     | new next          |
            //       ^                          ^
            //       |                          |
            //     tail                        tail
            //
            // 需要更新的情况(next 不是表尾节点):
            //
            // | next |     |  ==>     | new next          |    |
            //        ^                        ^
            //        |                        |
            //    old tail                 old tail
            //
            // 更新之后:
            //
            // | new next          |    |
            //                     ^
            //                     |
            //                  new tail
            // T = O(1)
            if((zl+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))) != np) {
                ZIPLIST_TAIL_OFFSET(zl) =
                   intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+extra);
            }
 
            /* Move the tail to the back. */
            // 向后移动 cur 节点之后的数据,为 cur 的新 header 腾出空间
            //
            // 示例:
            //
            // | header | value |  ==> | header |    | value |  ==> | header      | value |
            //                                   |<-->|
            //                            为新 header 腾出的空间
            // T = O(N)
            memmove(np+rawlensize,
                np+next.prevrawlensize,
               curlen-noffset-next.prevrawlensize-1);
            // 将新的前一节点长度值编码进新的 next 节点的 header
            // T = O(1)
            zipPrevEncodeLength(np,rawlen);
 
            /* Advance the cursor */
            // 移动指针,继续处理下个节点
            p += rawlen;
            curlen += extra;
        } else {
            if (next.prevrawlensize >rawlensize) {
                /* This would result inshrinking, which we want to avoid.
                 * So, set "rawlen"in the available bytes. */
                // 执行到这里,说明 next 节点编码前置节点的 header 空间有 5 字节
                // 而编码 rawlen 只需要 1 字节
                // 但是程序不会对 next 进行缩小,
                // 所以这里只将 rawlen 写入 5 字节的 header 中就算了。
                // T = O(1)
               zipPrevEncodeLengthForceLarge(p+rawlen,rawlen);
            } else {
                // 运行到这里,
                // 说明 cur 节点的长度正好可以编码到 next 节点的 header 中
                // T = O(1)
               zipPrevEncodeLength(p+rawlen,rawlen);
            }
 
            /* Stop here, as the raw length of"next" has not changed. */
            break;
        }
    }
 
    return zl;
}

       连锁更新的最坏复杂度达到O(N2),这看似是一个严重的性能问题。然而真正造成性能问题的概率很低:只要不是多个连续,长度介于250-253字节之间的节点的情况连锁更新就不会被引发——即使真的引起连锁更新,只要更新的节点数目不多,依然不会引起性能问题。

 

 

你可能感兴趣的:(Redis)