Redis设计与实现 笔记 第七章 压缩列表

压缩列表

压缩列表是列表键和哈希键的底层实现之一,当一个列表键值包含少量列表项,并且每个列表项,要么是小整数,要么是长度比较短的字符串,那么 Redis 就会使用压缩列表来做列表键的底层实现.

7.1 压缩列表的构成
zlbytes 4字节zltail 4字节zllen 2字节entryX 列表字节entryX 列表字节zlend 1字节
//zlbytes + zltail + zllen
#define ZIPLIST_HEADER_SIZE     (sizeof(uint32_t)*2+sizeof(uint16_t))

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;

    return zl;
}

zlbytes 用来记录整个压缩列表的内存字节数.
zltail 用来记录压缩列表节点距离起始位置有多少字节.
zlen 用来揭露压缩列表的节点数量,但该字段只有2个字节,所以当字节数超过65535时,节点的真是数量需要遍历整个压缩表表才能知晓.
entryX 节点的内容,真正用来储存内容的独享.
zlend 标记作用,0XFF用于标记压缩列表的末端.

7.2 压缩列表节点的构成
typedef struct 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;

节点由以下部分组成

prevrawlensize+ prevrawlenlensize + lenheadersizeencodingp

书上的结构与最新的代码还是有区别,但大体思路是不变的,一部分来指向前一个节点,一部分来表示当前节点的编码方式,一部分来指向节点的真正内容

7.2.1 prevrawlensize+ prevrawlen|lensize + len|headersize

用来记录前一个节点的长度,本节点的起始指针减去该长度就位前一个节点的指针.
如果前一个节点的长度小于254字节, previous_entry_length 的长度为1个字节,剩余保存的是前一个字节的长度
如果前一个字节长度大于254字节, previous_entry_length 属性的长度将为5个字节,第一个字节设置为0xFE,而之后的四个字节则用于保存前一个节点的长度

7.2.2 encoding

encoding 用来记录当前编码类型,表示content中记录的是类型的元素规则如下


/* Different encoding/length possibilities */
/*
 * 字符串编码和整数编码的掩码
 */
#define ZIP_STR_MASK 0xc0
#define ZIP_INT_MASK 0x30

/*
 * 字符串编码类型
 */
#define ZIP_STR_06B (0 << 6)
#define ZIP_STR_14B (1 << 6)
#define ZIP_STR_32B (2 << 6)

/*
 * 整数编码类型
 */
#define ZIP_INT_16B (0xc0 | 0<<4)
#define ZIP_INT_32B (0xc0 | 1<<4)
#define ZIP_INT_64B (0xc0 | 2<<4)
#define ZIP_INT_24B (0xc0 | 3<<4)
#define ZIP_INT_8B 0xfe

ZIP_STR_06B (0 << 6) 表示长度小于等于 63字节 的字节数组
ZIP_STR_14B (1 << 6) 表示长度小于等于 16383字节 的字节数组
ZIP_STR_32B (2 << 6) 表示长度小于等于 4294967295字节 的字节数组

ZIP_INT_16B (0xc0 | 0<<4) int16_t类型的整数
ZIP_INT_32B (0xc0 | 1<<4) int32_t类型的整数
ZIP_INT_64B (0xc0 | 2<<4) int64_t类型的整数
ZIP_INT_24B (0xc0 | 3<<4) 24位由符号整数
ZIP_INT_8B 0xfe

7.2.3 content = p

在最新代码中, p 单纯只用来存放内容

7.3 连锁更新

当多个节点大小处于254时,当插入一个大于254节点时,插入节点的后一个节点无法用1个字节来进行前一个节点长度的描述,所以需要进行更新操作,当一连串的节点都需要更新时,就会发生连锁更新.

/* When an entry is inserted, we need to set the prevlen field of the next
 * entry to equal the length of the inserted entry. It can occur that this
 * length cannot be encoded in 1 byte and the next entry needs to be grow
 * a bit larger to hold the 5-byte encoded prevlen. This can be done for free,
 * because this only happens when an entry is already being inserted (which
 * causes a realloc and memmove). However, encoding the prevlen may require
 * that this entry is grown as well. This effect may cascade throughout
 * the ziplist when there are consecutive entries with a size close to
 * ZIP_BIGLEN, so we need to check that the prevlen can be encoded in every
 * consecutive entry.
 *
 * 当将一个新节点添加到某个节点之前的时候,
 * 如果原节点的 header 空间不足以保存新节点的长度,
 * 那么就需要对原节点的 header 空间进行扩展(从 1 字节扩展到 5 字节)。
 *
 * 但是,当对原节点进行扩展之后,原节点的下一个节点的 prevlen 可能出现空间不足,
 * 这种情况在多个连续节点的长度都接近 ZIP_BIGLEN 时可能发生。
 *
 * 这个函数就用于检查并修复后续节点的空间问题。
 *
 * Note that this effect can also happen in reverse, where the bytes required
 * to encode the prevlen field can shrink. This effect is deliberately ignored,
 * because it can cause a "flapping" effect where a chain prevlen fields is
 * first grown and then shrunk again after consecutive inserts. Rather, the
 * field is allowed to stay larger than necessary, because a large prevlen
 * field implies the ziplist is holding large entries anyway.
 *
 * 反过来说,
 * 因为节点的长度变小而引起的连续缩小也是可能出现的,
 * 不过,为了避免扩展-缩小-扩展-缩小这样的情况反复出现(flapping,抖动),
 * 我们不处理这种情况,而是任由 prevlen 比所需的长度更长。
 
 * The pointer "p" points to the first entry that does NOT need to be
 * updated, i.e. consecutive fields MAY need an update. 
 *
 * 注意,程序的检查是针对 p 的后续节点,而不是 p 所指向的节点。
 * 因为节点 p 在传入之前已经完成了所需的空间扩展工作。
 *
 * T = O(N^2)
 */
static unsigned 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" 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". */
            // 执行到这里,表示 next 空间的大小不足以编码 cur 的长度
            // 所以程序需要对 next 节点的(header 部分)空间进行扩展

            // 记录 p 的偏移量
            offset = p-zl;
            // 计算需要增加的节点数量
            extra = rawlensize-next.prevrawlensize;
            // 扩展 zl 的大小
            // T = O(N)
            zl = ziplistResize(zl,curlen+extra);
            // 还原指针 p
            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. */
            // 当 next 节点不是表尾节点时,更新列表到表尾节点的偏移量
            // 
            // 不用更新的情况(next 为表尾节点):
            //
            // |     | next |      ==>    |     | 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 in shrinking, 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(N) ,再执行迁移操作的话,连锁更新的醉话复杂度为 O(N^2).
但是实际上可能不会这么可怕,该原因造成性能问题的几率是很低的.
首先,压缩列表里要恰好有多个连续的,长度介于250字节至253之间的节点连锁更新才有可能被引发,这种情况并不多见.
其次,即使出现连锁更新,但是只要更新的节点数量不多,就不会对性能造成任何影响.
综上,ziplistpush 命令的平均复杂度仅为 O(N),可以放心使用

总结

压缩列表是一种为节约内存而开发的顺序性数据结构.
压缩列表被用作列表键和哈希键的底层实现之一.
压缩列表可以包含多个节点,每个节点可以保存一个字节数组或者整数值.
添加新节点到压缩列表,或者从压缩列表中删除节点,都可能还会引发连锁更新操作,但是概率不高,可以放心使用

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值