美文网首页工作生活
「Redis源码解读」—数据结构(五)压缩列表

「Redis源码解读」—数据结构(五)压缩列表

作者: wh4763 | 来源:发表于2019-06-30 17:15 被阅读0次

知识点

  • 压缩列表是一种为节约内存而开发的顺序型数据结构
  • 压缩列表被用作列表键和哈希键的底层实现之一,当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么redis就会使用压缩列表来做键表列的底层实现。
  • 另外,当一个哈希键只包含少量键值对,并且每个键值对的键和值要么就是小整数值,要么就是长度比较短的字符串,那么redis就会使用压缩列表来做哈希键的底层实现。
  • 压缩列表可以包含多个节点,每个节点可以保存一个字节数或者整数值
  • 添加新节点到压缩列表,或者从压缩列表中删除节点,可能引起连锁更新操作,但这种操作出现的概率不高。

压缩列表概述

压缩列表是一种编码过的“链表”旨在实现高效的内存管理。它可以存储整数和字符串,整数以小端序存储,字符串则以字节数组存储



其中zlbytes、zltail、zllen 是 压缩列表头( ziplist header ),entry1 到 entryN 是列表的结点部分,zlen 是结尾标记。

  • zlbytes:记录整个压缩列表占用的内存字节数,在对压缩列表进行内存重分配时或者计算zlend的位置时使用
  • zltail:记录压缩列表表尾节点距离压缩列表的起始地址有多少字节,通过这个偏移量,redis无需遍历整个压缩列表就可以确定表尾节点的地址
  • zllen:记录了压缩列表包含的节点数量
  • entry:压缩列表的各个节点
  • end:用于标记压缩列表末端

结构体

/*
 * 节点结构
 */
typedef struct zlentry {
    unsigned int prevrawlensize,    // 保存前一节点的长度所需的长度 
                 prevrawlen;        // 前一节点的长度
    unsigned int lensize,           // 保存节点的长度所需的长度
                 len;               // 节点的长度
    unsigned int headersize;        // header 长度
    unsigned char encoding;         // 编码方式
    unsigned char *p;               // 内容
} zlentry;

连锁更新

  • 考虑这样一种情况, 有多个连续的、长度介于 250 字节到 253 字节之间的结点 e1 到 eN 。由于长度范围在 [250, 253],所以每个结点的 prevlen 字段只需要一个字节。其中 e1 的 prevlen 字段等于 0。如图所示:


  • 这时,我们在列表头插入一个新结点 new ,这个新结点的长度大于等于 254 。如图所示:


  • e1 的 prevlen 字段就表示 new 结点的长度(大于等于254),需要从 1字节 变为 5字节。换言之,e1 结点的长度增加了4。 e1 结点原本的长度范围在 [250,253],现在变成了 [254, 257],那么 e2 结点的 prevlen 字段也从原来的 1 字节变成了 5 字节。就这样以此类推, 一直到 eN,所有结点的 prevlen 字段都从 1 字节 增长为 5 字节。这就是连锁更新。
  • 连锁更新最坏情况下会触发 n 次空间重分配 (realloc) 和内存移动 (memmove),每次空间重分配的复杂度是O(n),所以连锁更新的最坏时间复杂度是O(n^2)。但是由于要有恰好多个长度 [250, 253] 的结点才会触发连锁更新,连续三五个在这个范围内的结点是不会影响性能的。

API源码

/*
 * 新创建一个空 ziplist
 * 返回值:新创建的 ziplist
 */
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;
}

/* Resize the ziplist. */
/*
 * 对 zl 进行空间重非配,并更新相关属性
 * 返回值:更新后的 ziplist
 */
static unsigned char *ziplistResize(unsigned char *zl, unsigned int len) {
    // 重分配
    zl = zrealloc(zl,len);
    // 更新长度
    ZIPLIST_BYTES(zl) = intrev32ifbe(len);
    // 设置表尾
    zl[len-1] = ZIP_END;
    return zl;
}
/* Insert item at "p". */
/*
 * 添加保存给定元素 s 的新节点到地址 p
 * 返回值:删除元素后的 ziplist
 */
static unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {
    size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)),
                    reqlen,
                    prevlen = 0;
    size_t offset;
    int nextdiff = 0;
    unsigned char encoding = 0;
    long long value = 123456789; /* initialized to avoid warning. Using a value
                                    that is easy to see if for some reason
                                    we use it uninitialized. */
    zlentry entry, tail;

    /* Find out prevlen for the entry that is inserted. */
    // 如果 p 之后不是没有节点(不是插入到末端)
    // 那么取出节点相关资料,以及 prevlen
    if (p[0] != ZIP_END) {
        entry = zipEntry(p);
        prevlen = entry.prevrawlen;
    } else {
        // 获取列表最后一个节点(表尾)的地址
        unsigned char *ptail = ZIPLIST_ENTRY_TAIL(zl);
        // 如果地址之后不是末端(也即是,列表至少有一个节点)
        if (ptail[0] != ZIP_END) {
            // 保存 ptail 指向的节点的空间长度
            prevlen = zipRawEntryLength(ptail);
        }
    }

    /* See if the entry can be encoded */
    // 查看能否将新值保存为整数
    // 如果可以的话返回 1 ,
    // 并将新值保存到 value ,编码形式保存到 encoding
    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;
    }
    /* We need space for both the length of the previous entry and
     * the length of the payload. */
    // 计算编码 prevlen 所需的长度
    reqlen += zipPrevEncodeLength(NULL,prevlen);
    // 计算编码 slen 所需的长度
    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. */
    // 如果添加的位置不是表尾,那么必须确定后继节点的 prevlen 空间
    // 足以保存新节点的编码长度
    // zipPrevLenByteDiff 的返回值有三种可能:
    // 1)新旧两个节点的编码长度相等,返回 0
    // 2)新节点编码长度 > 旧节点编码长度,返回 5 - 1 = 4
    // 3)旧节点编码长度 > 新编码节点长度,返回 1 - 5 = -4
    nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0;

    /* Store offset because a realloc may change the address of zl. */
    // 保存偏移量,因为重分配空间有可能改变 zl 的内存地址
    offset = p-zl;
    // 重分配空间,并更新长度属性和表尾
    // 新空间长度 = 现有长度 + 新节点所需长度 + 编码新节点长度所需的长度差
    // O(N)
    zl = ziplistResize(zl,curlen+reqlen+nextdiff);
    // 更新 p 的指针
    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 */
        // 向右移动移原有数据,为新节点让出空间
        // 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);
    }

    /* 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;
        // O(N^2)
        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;
}

/*
 * 将新元素插入为列表的表头节点或者表尾节点
 * 返回值:添加操作完成后的 ziplist
 */
unsigned char *ziplistPush(unsigned char *zl, unsigned char *s, unsigned int slen, int where) {
    unsigned char *p;
    p = (where == ZIPLIST_HEAD) ? ZIPLIST_ENTRY_HEAD(zl) : ZIPLIST_ENTRY_END(zl);
    return __ziplistInsert(zl,p,s,slen);
}
/*
 * 返回指向 p 的下一个节点的指针,
 * 如果 p 已经到达表尾,那么返回 NULL 。
 */
unsigned char *ziplistNext(unsigned char *zl, unsigned char *p) {
    ((void) zl);

    /* "p" could be equal to ZIP_END, caused by ziplistDelete,
     * and we should return NULL. Otherwise, we should return NULL
     * when the *next* element is ZIP_END (there is no next entry). */
    if (p[0] == ZIP_END) {
        return NULL;
    }

    // 指向下一节点,O(1)
    p += zipRawEntryLength(p);
    if (p[0] == ZIP_END) {
        return NULL;
    }

    return p;
}
/*
 * 返回 p 的前一个节点
 */
unsigned char *ziplistPrev(unsigned char *zl, unsigned char *p) {
    zlentry entry;

    /* Iterating backwards from ZIP_END should return the tail. When "p" is
     * equal to the first element of the list, we're already at the head,
     * and should return NULL. */
    // 这是表尾
    if (p[0] == ZIP_END) {
        p = ZIPLIST_ENTRY_TAIL(zl);
        return (p[0] == ZIP_END) ? NULL : p;
    // 到达表头,停止
    } else if (p == ZIPLIST_ENTRY_HEAD(zl)) {
        return NULL;
    } else {
        entry = zipEntry(p);
        assert(entry.prevrawlen > 0);
        return p-entry.prevrawlen;
    }
}
/*
 * 删除 p 所指向的节点,
 * 并原地更新 p 指针,让它指向被删除节点的后一个节点,
 * 使得可以迭代地进行删除
 * 返回值:删除完成后的 ziplist
 */
unsigned char *ziplistDelete(unsigned char *zl, unsigned char **p) {
    size_t offset = *p-zl;
    zl = __ziplistDelete(zl,*p,1);

    /* Store pointer to current element in p, because ziplistDelete will
     * do a realloc which might result in a different "zl"-pointer.
     * When the delete direction is back to front, we might delete the last
     * entry and end up with "p" pointing to ZIP_END, so check this. */
    *p = zl+offset;
    return zl;
}
/*
 * 将 p 所指向的节点的属性和 sstr 以及 slen 进行对比,
 * 如果相等则返回 1 。
 */
unsigned int ziplistCompare(unsigned char *p, unsigned char *sstr, unsigned int slen) {
    zlentry entry;
    unsigned char sencoding;
    long long zval, sval;

    // p 是表尾?
    if (p[0] == ZIP_END) return 0;

    // 获取节点属性
    entry = zipEntry(p);
    // 对比字符串
    if (ZIP_IS_STR(entry.encoding)) {
        /* Raw compare */
        if (entry.len == slen) {
            // O(N)
            return memcmp(p+entry.headersize,sstr,slen) == 0;
        } else {
            return 0;
        }
    // 对比整数
    } else {
        /* Try to compare encoded values. Don't compare encoding because
         * different implementations may encoded integers differently. */
        if (zipTryEncoding(sstr,slen,&sval,&sencoding)) {
          zval = zipLoadInteger(p+entry.headersize,entry.encoding);
          return zval == sval;
        }
    }

    return 0;
}
/* Return length of ziplist. */
/*
 * 返回 ziplist 的长度
 */
unsigned int ziplistLen(unsigned char *zl) {
    unsigned int len = 0;
    // 节点的数量 < UINT16_MAX
    if (intrev16ifbe(ZIPLIST_LENGTH(zl)) < UINT16_MAX) {
        // 长度保存在一个 uint16 整数中
        len = intrev16ifbe(ZIPLIST_LENGTH(zl));
    // 节点的数量 >= UINT16_MAX
    } else {
        // 遍历整个 ziplist ,计算长度
        unsigned char *p = zl+ZIPLIST_HEADER_SIZE;
        while (*p != ZIP_END) {
            p += zipRawEntryLength(p);
            len++;
        }

        /* Re-store length if small enough */
        if (len < UINT16_MAX) ZIPLIST_LENGTH(zl) = intrev16ifbe(len);
    }
    return len;
}

/* Return ziplist blob size in bytes. */
/*
 * 返回整个 ziplist 的空间大小
 */
size_t ziplistBlobLen(unsigned char *zl) {
    return intrev32ifbe(ZIPLIST_BYTES(zl));
}


相关文章

网友评论

    本文标题:「Redis源码解读」—数据结构(五)压缩列表

    本文链接:https://www.haomeiwen.com/subject/susbcctx.html