Redis6系列3-底层数据结构(压缩列表)

231 阅读13分钟

欢迎大家关注 github.com/hsfxuebao ,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈

我们接着分析Redis6的底层数据结构-压缩列表。

1. 压缩列表的构成

压缩列表是由一些列特殊编码的连续内存块组成的顺序型(sequential)数据结构。一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整型数值。

ziplist是一个经过特殊编码的 双向链表 ,它不存储指向上一个链表节点和指向下一个链表节点的指针,而是存储上一个节点长度和当前节点长度,通过牺牲部分读写性能,来换取高效的内存空间利用率,节约内存,是一种时间换空间的思想。只用在字段个数少,字段值小的场景里面。

压缩列表是 Redis 为节约空间而实现的一系列特殊编码的连续内存块组成的顺序型数据结构, 本质上是字节数组。在模型上将这些连续的数组分为3大部分,分别是header+entry集合+end, 其中header由zlbytes+zltail+zllen组成, entry是节点, zlend是一个单字节255(1111 1111),用做ZipList的结尾标识符。结构图如下所示:
image.png

压缩列表各个组成部门的详细说明:

属性类型长度用途
zlbytesuint32_t4字节记录整个压缩列表占用的内存字节数,对 ziplist 进行内存重分配,或者计算 zlend 的位置时使用
zltailuint32_t4字节记录压缩列表表尾节点距离压缩列表的起始位置有多少字节,通过这个偏移量,程序无需遍历整个压缩列表就可以确定表尾节点的地址
zllenuint16_t2字节记录了压缩列表包含的节点数量,当这个属性的值小于 UINT16_MAX(65536)时,这个属性的值就是压缩列表包含节点的数量,当这个值等于 UINT16_MAX时,节点的真实数量需要遍历整个压缩列表才能计算得出
entryX列表节点不定压缩列表包含的各个节点,节点的长度由节点保存的内容决定
zlenduint8_t1字节特殊值 0xFF(十进制 255),用于标记压缩列表的末端

2. 压缩列表的节点

ziplist.c对应的zlentry结构为:

// 压缩列表节点
typedef struct zlentry {
    // 上一节点的长度所占的字节数,有1字节和5字节两种
    unsigned int prevrawlensize; 
    // 上一个节点的长度
    unsigned int prevrawlen;  
    // 编码当前节点长度len所需要的字节数
    unsigned int lensize;  
    // 当前节点的长度                              
    unsigned int len;  
    // 当前节点的header大小,headersize = lensize + prevrawlensize
    unsigned int headersize;    
    // 当前节点的编码格式
    unsigned char encoding;      
    // 当前节点指针
    unsigned char *p;            
} zlentry;

每个压缩列表的节点可以保存一个字节数组或者一个整数值,其中,字节数组可以是以下三种长度中的一种:

  1. 长度小于等于 63 ( [公式] )字节的字符数组。
  2. 长度小于等于 16383 ( [公式] ) 字节的字符数组。
  3. 长度小于等于 4294967295 ( [公式] )字节的字符数组。

整数值可以是以下6种长度中的一种:

  1. 4 位长,介于 0 至 12 之间的无符号整数。
  2. 1 字节长,有符号整数。
  3. 3 字节长,有符号整数。
  4. int16_t 类型整数。
  5. int32_t 类型整数。
  6. int64_t 类型整数。 压缩列表zlentry节点结构:每个zlentry由 前一个节点的长度 、encoding和entry-data三部分组成: image.png

2.1 previous_entry_length

节点的 previous_entry_length 域以字节为单位,记录了压缩列表中前一个节点的总长度,该域的长度可以是 1 字节 或者 5 字节

  • 如果前一节点的长度小于 254 字节,那么 previous_entry_length 域的长度为 1 字节,前一节点的长度就保存在这一个字节里面。
  • 如果前一节点的长度大于等于 254 字节,那么 previous_entry_length 域的长度为 5 字节,其中第一字节会被设置为 0xFE(十进制值 254) ,而之后的四个字节则用于保存前一节点的长度。 image.png 以上图为例,当上一个节点的总长度是 1000 1000,十进制值即 136 要小于 254 时,当前节点的 previous_entry_length 域长度就是 1字节;当上一个节点的总长度是 10 0111 0110 0110,十进制值即 10086 要大于 254 时,当前节点的 previous_entry_length 域长度就是 5字节,其第一个字节的值为 1111 1110(254),后四个字节用于保存前一个节点的长度。 对应的redis源码如下:
int zipStorePrevEntryLengthLarge(unsigned char *p, unsigned int len) {
    if (p != NULL) {
        p[0] = ZIP_BIG_PREVLEN;         //第一个字节固定存放254
        memcpy(p+1,&len,sizeof(len));   //上一个节点的长度存放在后四个字节
        memrev32ifbe(p+1);
    }
    return 1+sizeof(len);   //sizeof(len)的值为4
}

/* Encode the length of the previous entry and write it to "p". Return the
 * number of bytes needed to encode this length if "p" is NULL. */
unsigned int zipStorePrevEntryLength(unsigned char *p, unsigned int len) {
    if (p == NULL) {
        return (len < ZIP_BIG_PREVLEN) ? 1 : sizeof(len)+1;
    } else {
        if (len < ZIP_BIG_PREVLEN) {    //上一个节点的长度小于254
            p[0] = len;                 //preventrylength域只占一个字节
            return 1;
        } else {
            return zipStorePrevEntryLengthLarge(p,len);
        }
    }
}

/* Return the number of bytes used to encode the length of the previous
 * entry. The length is returned by setting the var 'prevlensize'. */
#define ZIP_DECODE_PREVLENSIZE(ptr, prevlensize) do {                          \
    if ((ptr)[0] < ZIP_BIG_PREVLEN) {                                          \
        (prevlensize) = 1;                                                     \
    } else {                                                                   \
        (prevlensize) = 5;                                                     \
    }                                                                          \
} while(0);

压缩列表的遍历:
通过指向表尾节点的位置指针p1, 减去节点的previous_entry_length,得到前一个节点起始地址的指针。如此循环,从表尾遍历到表头节点。从表尾向表头遍历操作就是使用这一原理实现的,只要我们拥有了一个指向某个节点起始地址的指针,那么通过这个指针以及这个节点的previous_entry_length属性程序就可以一直向前一个节点回溯,最终到达压缩列表的表头节点。

2.2 enconding

节点的 encoding 域用于记录 content 域所保存数据的类型和长度,该域的长度可以是 1 字节 、2 字节 或 5 字节encoding 域可以分成两部分:encoding type 和 data length,如下图所示: image.png

  1. encoding type 部分是固定 2 bit 长度,用于记录 content 域的数据类型,其值可以是 00011011

  2. 000110 表示 content 域保存着字符数组。

  3. 11 表示 content 域保存着整数。

  4. data length 部分的长度为 encoding 域的长度减去 2 bit,其值为 content 域数据的长度。

如果 content 域存放的数据是字符数组类型,encoding 域生成的源码如下:

unsigned int zipStoreEntryEncoding(unsigned char *p, unsigned char encoding, unsigned int rawlen) {
    //len字段表示 encoding 域所占字节,len先初始化1个字节
    unsigned char len = 1, buf[5];

    if (ZIP_IS_STR(encoding)) {
        /* Although encoding is given it may not be set for strings,
         * so we determine it here using the raw length. */
        if (rawlen <= 0x3f) {
            if (!p) return len;
            buf[0] = ZIP_STR_06B | rawlen;
        } else if (rawlen <= 0x3fff) {
            len += 1;   //占2个字节
            if (!p) return len;
            buf[0] = ZIP_STR_14B | ((rawlen >> 8) & 0x3f);  //第一个字节的头两位固定为01
            buf[1] = rawlen & 0xff;
        } else {
            len += 4;   //占5个字节
            if (!p) return len;
            buf[0] = ZIP_STR_32B;   //第一个字节的值固定为 1000 0000
            buf[1] = (rawlen >> 24) & 0xff;
            buf[2] = (rawlen >> 16) & 0xff;
            buf[3] = (rawlen >> 8) & 0xff;
            buf[4] = rawlen & 0xff;
        }
    } else {
        /* Implies integer encoding, so length is always 1. */
        if (!p) return len;
        buf[0] = encoding;  //如果是整型,只占用1个字节
    }

    /* Store this length at p. */
    memcpy(p,buf,len);
    return len;
}

如果 content 域存放的数据是整数类型,encoding 域生成的源码如下:

oid zipSaveInteger(unsigned char *p, int64_t value, unsigned char encoding) {
    int16_t i16;
    int32_t i32;
    int64_t i64;
    if (encoding == ZIP_INT_8B) {
        ((int8_t*)p)[0] = (int8_t)value;
    } else if (encoding == ZIP_INT_16B) {
        i16 = value;
        memcpy(p,&i16,sizeof(i16));
        memrev16ifbe(p);
    } else if (encoding == ZIP_INT_24B) {
        i32 = value<<8;
        memrev32ifbe(&i32);
        memcpy(p,((uint8_t*)&i32)+1,sizeof(i32)-sizeof(uint8_t));
    } else if (encoding == ZIP_INT_32B) {
        i32 = value;
        memcpy(p,&i32,sizeof(i32));
        memrev32ifbe(p);
    } else if (encoding == ZIP_INT_64B) {
        i64 = value;
        memcpy(p,&i64,sizeof(i64));
        memrev64ifbe(p);
    } else if (encoding >= ZIP_INT_IMM_MIN && encoding <= ZIP_INT_IMM_MAX) {
        /* Nothing to do, the value is stored in the encoding itself. */
    } else {
        assert(NULL);
    }
}

/* Read integer encoded as 'encoding' from 'p' */
int64_t zipLoadInteger(unsigned char *p, unsigned char encoding) {
    int16_t i16;
    int32_t i32;
    int64_t i64, ret = 0;
    if (encoding == ZIP_INT_8B) {
        ret = ((int8_t*)p)[0];
    } else if (encoding == ZIP_INT_16B) {
        memcpy(&i16,p,sizeof(i16));
        memrev16ifbe(&i16);
        ret = i16;
    } else if (encoding == ZIP_INT_32B) {
        memcpy(&i32,p,sizeof(i32));
        memrev32ifbe(&i32);
        ret = i32;
    } else if (encoding == ZIP_INT_24B) {
        i32 = 0;
        memcpy(((uint8_t*)&i32)+1,p,sizeof(i32)-sizeof(uint8_t));
        memrev32ifbe(&i32);
        ret = i32>>8;
    } else if (encoding == ZIP_INT_64B) {
        memcpy(&i64,p,sizeof(i64));
        memrev64ifbe(&i64);
        ret = i64;
    } else if (encoding >= ZIP_INT_IMM_MIN && encoding <= ZIP_INT_IMM_MAX) {
        ret = (encoding & ZIP_INT_IMM_MASK)-1;  //这里减了1,所以4bit整数的值是从0到12
    } else {
        assert(NULL);
    }
    return ret;
}

下图列出了 encoding 域的所有可能情况: image.png

2.3 content

节点的 content 域负责保存当前节点的值,节点值可以是字符数组或者整数,值的类型和长度记录在节点的 encoding 域。

3. 连锁更新

在一个压缩列表种,有多个连续的、长度介于 250 字节到 253 字节之间的节点 entry1 至 entryN,因为这些节点长度都小于 254 字节,所以记录这些节点长度的后一个节点的 previous_entry_length 域的都是 1 字节长。

这时,在 entry1 节点前面插入一个新节点 entryNew,如果 entryNew 的长度大于等于 254 字节, entry1 节点的 previous_entry_length 域就会由 1 字节扩展到 5 字节,entry1 节点整体长度就增加了4个字节,超过了 254 字节。

之后,entry2 节点的 previous_entry_length 域也要从 1 字节扩展到 5 字节,其整体长度也超过了 254 字节。相应的,由于扩展 entry2 节点也会引发 entry3 节点的扩展,entry3 节点的扩展又会引起 entry4 节点的扩展,这样,程序就需要不断地对压缩列表进行空间重分配,直到 entryN 为止。

Redis 将这种在特殊情况下产生的连续多次空间扩展操作称之为 连锁更新(cascade update)

不仅仅是插入节点会引起连锁更新,删除节点也可能会引发连锁更新。连锁更新在最坏的情况下需对压缩列表执行 N 次空间重新分配操作,而每次空间重分配的最坏复杂度为 O(N) ,所以连锁更新的最坏复杂度为 O(N2N^2) 。连锁更新的复杂度较高,但在实际中,发生这种情况的几率并不大。 连锁更新的源码如下:

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;

    while (p[0] != ZIP_END) {
        //获取当前节点信息
        zipEntry(p, &cur);
        //计算当前节点总长度
        rawlen = cur.headersize + cur.len;
        //计算rawlen长度对应的 previous_entry_length 域的长度
        rawlensize = zipStorePrevEntryLength(NULL,rawlen);

        /* Abort if there is no next entry. */
        //当前节点是最后一个节点,则无需再遍历
        if (p[rawlen] == ZIP_END) break;
        //获取下一个节点信息
        zipEntry(p+rawlen, &next);

        /* Abort when "prevlen" has not changed. */
        //当前节点的总长度没有变化,则无需再遍历
        if (next.prevrawlen == rawlen) break;

        //计算next节点的 previous_entry_length 域是需要扩展还是收缩
        if (next.prevrawlensize < rawlensize) {
            /* The "prevlen" field of "next" needs more bytes to hold
             * the raw length of "cur". */
            //next节点的 previous_entry_length 域不够容纳当前节点的长度
            //计算当前节点距离压缩列表起始处的地址偏移量
            offset = p-zl;
            //计算next节点需要扩展的字节数
            extra = rawlensize-next.prevrawlensize;
            //扩容
            zl = ziplistResize(zl,curlen+extra);
            //重新定位到当前节点的内存地址
            p = zl+offset;

            /* Current pointer and offset for next element. */
            //移动到next节点的起始位置
            np = p+rawlen;
            //计算next节点相对于压缩列表起始位置的地址偏移量
            noffset = np-zl;

            /* Update tail offset when next element is not the tail element. */
            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. */
            //内存迁移
            //np:next节点的起始地址
            //rawlensize: 存下cur节点的长度数据需要的 previous_entry_length 域的长度
            //np+rawlensize:预留出next节点的 previous_entry_length 域长度
            //next.prevrawlensize:next节点的 previous_entry_length 域的长度
            //np+next.prevrawlensize:从next节点的encoding域开始迁移
            //curlen-noffset-next.prevrawlensize-1:内存迁移的范围为从next节点的encoding域
            //起始处到最后一个节点的末尾
            memmove(np+rawlensize,
                np+next.prevrawlensize,
                curlen-noffset-next.prevrawlensize-1);
            //修改next节点的previous_entry_length域的值
            zipStorePrevEntryLength(np,rawlen);

            /* Advance the cursor */
            //移动游标,即以next节点作为监视对象,往后判断是否需要扩展
            p += rawlen;
            //压缩列表总长度增加
            curlen += extra;
        } else {
            //如果next节点当前的previous_entry_length域长度超出需要的大小,则强制保留现以内存
            //不进行缩小,因为仅浪费一点内存却省去了大量移动复制操作且后续增大时也无需再扩展
            if (next.prevrawlensize > rawlensize) {
                /* This would result in shrinking, which we want to avoid.
                 * So, set "rawlen" in the available bytes. */
                zipStorePrevEntryLengthLarge(p+rawlen,rawlen);
            } else {
                zipStorePrevEntryLength(p+rawlen,rawlen);
            }

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

4. 压缩列表API

4.1 创建压缩列表

​ 其源码如下:

#define ZIP_END 255         /* Special "end of ziplist" entry. */

/* Return total bytes a ziplist is composed of. */
#define ZIPLIST_BYTES(zl)       (*((uint32_t*)(zl)))

/* Return the offset of the last item inside the ziplist. */
#define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t))))

/* Return the length of a ziplist, or UINT16_MAX if the length cannot be
 * determined without scanning the whole ziplist. */
#define ZIPLIST_LENGTH(zl)      (*((uint16_t*)((zl)+sizeof(uint32_t)*2)))

/* The size of a ziplist header: two 32 bit integers for the total
 * bytes count and last item offset. One 16 bit integer for the number
 * of items field. */
#define ZIPLIST_HEADER_SIZE     (sizeof(uint32_t)*2+sizeof(uint16_t))

/* Size of the "end of ziplist" entry. Just one byte. */
#define ZIPLIST_END_SIZE        (sizeof(uint8_t))

/* Create a new empty ziplist. */
unsigned char *ziplistNew(void) {
    //没有节点时,压缩列表的长度就是头部和尾部的长度之和
    unsigned int bytes = ZIPLIST_HEADER_SIZE+ZIPLIST_END_SIZE;
    unsigned char *zl = zmalloc(bytes);
    ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);
    ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);
    ZIPLIST_LENGTH(zl) = 0; //因为是空的压缩列表,节点数量初始化为0
    zl[bytes-1] = ZIP_END;
    return zl;
}c

4.2 添加元素

​ 往压缩列表中添加元素有三种方式:头部添加、尾部添加、添加在指定节点之后。在 Redis 中对应的源码如下:

#define ZIPLIST_HEAD 0
#define ZIPLIST_TAIL 1

/* Return the pointer to the first entry of a ziplist. */
#define ZIPLIST_ENTRY_HEAD(zl)  ((zl)+ZIPLIST_HEADER_SIZE)

/* Return the pointer to the last entry of a ziplist, using the
 * last entry offset inside the ziplist header. */
#define ZIPLIST_ENTRY_TAIL(zl)  ((zl)+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl)))

//在头部或者尾部添加元素
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);
}

//在指定位置添加元素
/* Insert an entry at "p". */
unsigned char *ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {
    return __ziplistInsert(zl,p,s,slen);
}

​ 以上都可归结为在压缩列表的某个位置 p 后面添加元素,即 __ziplistInsert 函数,其实现如下:

/* Insert item at "p". */
unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {
    //获取压缩列表当前长度
    size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), reqlen;
    unsigned int prevlensize, 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 tail;

    /* Find out prevlen for the entry that is inserted. */
    if (p[0] != ZIP_END) {
        //不是尾部添加,获取前一个节点的长度
        ZIP_DECODE_PREVLEN(p, prevlensize, prevlen);
    } else {
        //如果是尾部添加,则指针移到最后一个节点的起始处
        unsigned char *ptail = ZIPLIST_ENTRY_TAIL(zl);
        //判断最后一个节点的起始位置是否是压缩列表的尾部
        //如果不是尾部,则获取最后一个节点的长度
        //否则这个压缩列表就是没用一个节点,插入节点的 previous_entry_length 域的值为 0
        if (ptail[0] != ZIP_END) {
            //获取最后一个节点的长度
            prevlen = zipRawEntryLength(ptail);
        }
    }

    //添加的元素可能时字符数组,也可能是整数
    //如果是字符数组,则传入的参数slen就是新节点数据的长度
    //如果是整数,要根据整数的实际类型来计算新节点数据的长度
    /* 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 zipStoreEntryEncoding 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. */
    //计算新增节点 previous_entry_length 域的长度
    reqlen += zipStorePrevEntryLength(NULL,prevlen);
    //计算新增节点 encoding 域的长度
    reqlen += zipStoreEntryEncoding(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. */
    int forcelarge = 0;
    //如果不是在尾部添加节点,要确保后一个节点的 previous_entry_length 域能够容纳新增节点的长度
    //如果不能容纳,则需要计算差值,用于后面的节点扩展
    //reqlen表示新增节点的总长度
    //nextdiff的值可以有三种情况:0-空间相等、4-需要更多空间、-4-空间富余
    nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0;
    if (nextdiff == -4 && reqlen < 4) {
        //很多人不明白当 nextdiff 等于 -4 时 reqlen 怎么可能会小于4
        //这里在文章下面会有图解
        nextdiff = 0;
        forcelarge = 1;
    }

    /* Store offset because a realloc may change the address of zl. */
    //因为 realloc 函数可能会改变 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 */
        //将插入位置之后的内容整个移动到新增节点之后
        //reqlen是新节点的长度,p+reqlen就是为新节点预留位置
        //nextdiff为后一个节点扩展或收缩的内存大小
        //curlen-offset-1+nextdiff为总共需要迁移的内存长度
        memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff);

        /* Encode this entry's raw length in the next entry. */
        //将新增节点的长度存在下一个节点的 previous_entry_length 域
        if (forcelarge)
            zipStorePrevEntryLengthLarge(p+reqlen,reqlen);
        else
            zipStorePrevEntryLength(p+reqlen,reqlen);

        /* Update offset for tail */
        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. */
        //tail中记录了p+reqlen处节点的信息
        zipEntry(p+reqlen, &tail);
        //判断p+reqlen处的节点是否是最后一个节点
        if (p[reqlen+tail.headersize+tail.len] != ZIP_END) {
            //zltail 需要加上nextdiff
            ZIPLIST_TAIL_OFFSET(zl) = 
                intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+nextdiff);
        }
    } else {
        /* This element will be the new tail. */
        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;
        //进行连锁更新
        zl = __ziplistCascadeUpdate(zl,p+reqlen);
        p = zl+offset;
    }

    /* Write the entry */
    //保存新增节点的 previous_entry_length 域的值
    p += zipStorePrevEntryLength(p,prevlen);
    //保存新增节点的 encoding 域值
    p += zipStoreEntryEncoding(p,encoding,slen);
    //保存新增节点的 content 域的值
    if (ZIP_IS_STR(encoding)) {
        memcpy(p,s,slen);
    } else {
        zipSaveInteger(p,value,encoding);
    }
    //增加节点,压缩列表的节点数量加1
    ZIPLIST_INCR_LENGTH(zl,1);
    return zl;
}

​ 上述函数中,有这么一段代码很难理解:

if (nextdiff == -4 && reqlen < 4) {
        nextdiff = 0;
        forcelarge = 1;
    }

​ 我们很难理解,当 nextdiff == -4 时,reqlen 怎么可能会小于 4,并且当我们理解了 reqlen < 4 后,也会很疑惑 if 控制块里面代码的作用,这里将通过以下的图解说明:

​ 还有代码 memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff); 也需要重点说明以下 ,其中p-nextdiff 是指定内存搬迁的起始位置,因为插入新节点可能会导致后一个节点的 previous_entry_length 域扩张或收缩,如果 nextdiff = 4,则是扩张,后一个节点的 previous_entry_length 域由 1 个字节变成 5个字节,为了保证内存的连续性,这多出的 4 个字节要从前面划入,先不用管这多出的 4 个字节内存的内容,只是把内存空间预留出来,后面代码会修改内容。nextdiff = -4 时也是相同的原理。可以参考以下的内存迁移示意图:

4.3 删除节点

​ 压缩列表删除节点的源码如下:

/* Delete "num" entries, starting at "p". Returns pointer to the ziplist. */
//从压缩列表的 P 位置删除 num 个节点
unsigned char *__ziplistDelete(unsigned char *zl, unsigned char *p, unsigned int num) {
    unsigned int i, totlen, deleted = 0;
    size_t offset;
    int nextdiff = 0;
    zlentry first, tail;

    //获取P位置的节点信息
    zipEntry(p, &first);
    for (i = 0; p[0] != ZIP_END && i < num; i++) {
        //移动到下一个节点的起始位置
        p += zipRawEntryLength(p);
        //deleted为实际要删除的节点数量
        deleted++;
    }

    //要删除的节点长度之和
    totlen = p-first.p; /* Bytes taken by the element(s) to delete. */
    if (totlen > 0) {
        //当前位置是否是压缩列表尾部
        if (p[0] != ZIP_END) {
            /* Storing `prevrawlen` in this entry may increase or decrease the
             * number of bytes required compare to the current `prevrawlen`.
             * There always is room to store this, because it was previously
             * stored by an entry that is now being deleted. */
            //计算要存下first节点的前一个节点的长度,p位置节点的previous_entry_length域需要
            //扩展的字节数
            nextdiff = zipPrevLenByteDiff(p,first.prevrawlen);

            /* Note that there is always space when p jumps backward: if
             * the new previous entry is large, one of the deleted elements
             * had a 5 bytes prevlen header, so there is for sure at least
             * 5 bytes free and we need just 4. */
            //先修改p位置节点的 previous_entry_length 域大小
            p -= nextdiff;
            //再修改P位置节点的 previous_entry_length域的值
            zipStorePrevEntryLength(p,first.prevrawlen);

            /* Update offset for tail */
            //更新最后一个节点到压缩列表开头的内存偏移量
            ZIPLIST_TAIL_OFFSET(zl) =
                intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))-totlen);

            /* 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. */
            //获取p位置的节点信息
            zipEntry(p, &tail);
            //判断tail节点是否是最后一个节点
            if (p[tail.headersize+tail.len] != ZIP_END) {
                //最后一个节点到压缩列表开头的内存偏移量还有计算上扩展或收缩的大小
                ZIPLIST_TAIL_OFFSET(zl) =
                   intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+nextdiff);
            }

            /* Move tail to the front of the ziplist */
            //内存迁移
            memmove(first.p,p,
                intrev32ifbe(ZIPLIST_BYTES(zl))-(p-zl)-1);
        } else {
            /* The entire tail was deleted. No need to move memory. */
            ZIPLIST_TAIL_OFFSET(zl) =
                intrev32ifbe((first.p-zl)-first.prevrawlen);
        }

        /* Resize and update length */
        offset = first.p-zl;
        zl = ziplistResize(zl, intrev32ifbe(ZIPLIST_BYTES(zl))-totlen+nextdiff);
        ZIPLIST_INCR_LENGTH(zl,-deleted);
        p = zl+offset;

        /* 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)
            //连锁更新
            zl = __ziplistCascadeUpdate(zl,p);
    }
    return zl;
}

4.4 查找元素

​ 压缩列表查找元素是通过 ziplistFind 函数来完成的,其具体实现的源码如下:

/* Find pointer to the entry equal to the specified entry. Skip 'skip' entries
 * between every comparison. Returns NULL when the field could not be found. */
/*
 * p 是开始查找的压缩列表节点地址
 * vstr 是要查找的元素内容
 * vlen 是要查找的元素长度
 * skip 是每查找一次跳过的元素个数
*/
unsigned char *ziplistFind(unsigned char *p, unsigned char *vstr, unsigned int vlen, unsigned int skip) {
    int skipcnt = 0;
    unsigned char vencoding = 0;
    long long vll = 0;

    while (p[0] != ZIP_END) {
        unsigned int prevlensize, encoding, lensize, len;
        unsigned char *q;

        //获取 previous_entry_length 域的长度
        ZIP_DECODE_PREVLENSIZE(p, prevlensize);
        //获取 encoding 域的长度
        ZIP_DECODE_LENGTH(p + prevlensize, encoding, lensize, len);
        //指针移到 content 域起始位置
        q = p + prevlensize + lensize;

        if (skipcnt == 0) {
            /* Compare current entry with specified entry */
            if (ZIP_IS_STR(encoding)) {
                //如果当前节点的内容是字符串,则比较字符串的长度及内容是否相同
                if (len == vlen && memcmp(q, vstr, vlen) == 0) {
                    //要查找的元素在当前节点
                    return p;
                }
            } else {
                /* Find out if the searched field can be encoded. Note that
                 * we do it only the first time, once done vencoding is set
                 * to non-zero and vll is set to the integer value. */
                if (vencoding == 0) {
                    //将要查找的元素编码为整数
                    if (!zipTryEncoding(vstr, vlen, &vll, &vencoding)) {
                        /* If the entry can't be encoded we set it to
                         * UCHAR_MAX so that we don't retry again the next
                         * time. */                        
                        vencoding = UCHAR_MAX;
                    }
                    /* Must be non-zero by now */
                    assert(vencoding);
                }

                /* Compare current entry with specified entry, do it only
                 * if vencoding != UCHAR_MAX because if there is no encoding
                 * possible for the field it can't be a valid integer. */
                if (vencoding != UCHAR_MAX) {
                    //根据当前节点的元素编码方式,获得元素的内容
                    long long ll = zipLoadInteger(q, encoding);
                    //因为是整数,直接判断是否相等,相等即说明找到了
                    if (ll == vll) {
                        return p;
                    }
                }
            }

            /* Reset skip count */
            skipcnt = skip;
        } else {
            /* Skip entry */
            skipcnt--;
        }

        /* Move to next entry */
        //移动到下一个节点
        p = q + len;
    }

    return NULL;
}

5. 压缩列表和双端链表区别

  1. 普通的双向链表会有两个指针,在存储数据很小的情况下,我们存储的实际数据的大小可能还没有指针占用的内存大,得不偿失ziplist 是一个特殊的双向链表没有维护双向指针:prev next;而是存储上一个 entry的长度和 当前entry的长度,通过长度推算下一个元素在什么地方。牺牲读取的性能,获得高效的存储空间,因为(简短字符串的情况)存储指针比存储entry长度更费内存。这是典型的“时间换空间”。 

  2. 链表在内存中一般是不连续的,遍历相对比较慢,而ziplist可以很好的解决这个问题,普通数组的遍历是根据数组里存储的数据类型找到下一个元素的(例如int类型的数组访问下一个元素时每次只需要移动一个sizeof(int)就行),但是ziplist的每个节点的长度是可以不一样的,而我们面对不同长度的节点又不可能直接sizeof(entry),所以ziplist只好将一些必要的偏移量信息记录在了每一个节点里,使之能跳到上一个节点或下一个节点。 

  3. 头节点里有头节点里同时还有一个参数 len,和string类型提到的 SDS 类似,这里是用来记录链表长度的。因此 获取链表长度时不用再遍历整个链表, 直接拿到len值就可以了,这个时间复杂度是 O(1)

参考文档:
转自:# redis-6.06 底层数据结构——压缩列表
Redis 设计与实现(第一版)
Redis的一个历史bug及其后续改进
Redis学习笔记&源码阅读--压缩列表-操作