Redis初识-压缩列表| 8月更文挑战

430 阅读9分钟

这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战

1. 简介

压缩列表 ziplist 底层是一个字节数组,是 Redis 为了节约内存而设计的数据结构,可以存储多个元素,每个元素可以是字节数组或整数

压缩列表应用于 Redis 的有序集合、散列、列表:

  • 当有序集合的元素较少且都是短字符串时,使用的数据结构为压缩列表
  • 列表使用的数据结构是快速列表 quicklist ,由双向链表和压缩列表组合而成
  • 散列的底层结构也包含压缩列表

2. 压缩链表的存储结构

2.1 压缩列表

压缩列表结构

压缩列表的属性:

  • zlbytes:表示压缩列表的字节长度,占4字节,因此压缩列表最多有2^32-1个字节
  • zltail:表示尾元素相对于压缩列表首地址的偏移量,占4字节
  • zllen:表示元素的个数,占2字节,因此压缩列表存储元素个数不超过2^16-1,必须遍历整个压缩列表才能获取元素个数
  • entryX:表示压缩列表存储的元素,可以是字节数组或整数
  • zlend:表示压缩列表的尾部,占1字节,恒为0xFF

Redis 中压缩列表属性相关宏定义:

// ziplist.c

// zl 指向 zlbytes
#define ZIPLIST_BYTES(zl)       (*((uint32_t*)(zl)))

// zl+4 指向 zltail
#define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t))))

// zl+8 指向 zllen
#define ZIPLIST_LENGTH(zl)      (*((uint16_t*)((zl)+sizeof(uint32_t)*2)))

// 压缩列表头部大小10字节(zlbytes+zltail+zllen)
#define ZIPLIST_HEADER_SIZE     (sizeof(uint32_t)*2+sizeof(uint16_t))

// 压缩列表尾部大小1字节
#define ZIPLIST_END_SIZE        (sizeof(uint8_t))

// 压缩列表的头元素
#define ZIPLIST_ENTRY_HEAD(zl)  ((zl)+ZIPLIST_HEADER_SIZE)

// 压缩列表的尾元素
#define ZIPLIST_ENTRY_TAIL(zl)  ((zl)+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl)))

// 压缩列表元素的尾部
#define ZIPLIST_ENTRY_END(zl)   ((zl)+intrev32ifbe(ZIPLIST_BYTES(zl))-1)

2.2 压缩列表元素的编码结构

压缩列表元素编码结构

元素编码结构的属性:

  • previous_entry_length:表示前一个元素的字节长度,占1或5个字节
    • 前一个元素的字节长度小于254时,占1个字节
    • 前一个元素的字节长度大于等于254时,占5个字节,此时第1个字节固定为0xFE,后4个字节表示长度
    • 假设存在某个元素的首地址为 p ,那么 p-previous_entry_length 表示前一个元素的首地址,由此可以实现压缩列表从尾向头遍历
  • encoding:表示元素的编码,即 content 的数据类型(整数或字节数组)
  • content:存储元素的数据内容

为了节约内存,encoding 属性的长度是可变的:

encoding 编码encoding 长度content 类型
00 bbbbbb(6比特表示 content 长度)1字节最大长度为63的字节数组
01 bbbbbb cccccccc(14比特表示 content 长度)2字节最大长度为2^14-1的字节数组
10------ aaaaaaaa bbbbbbbb cccccccc dddddddd(32比特表示 content 长度)5字节最大长度为2^32-1的字节数组
110000001字节int16 整数
110100001字节int32 整数
111000001字节int64 整数
111100001字节24位整数
111111101字节8位整数
1111 xxxx1字节没有 content 属性,xxxx 表示区间 (0,12] 的整数
  • 根据 encoding 属性的第1个字节的前2位,可以判断 content 属性存储的是整数或字节数组(及其最大长度)
  • 当 content 存储的是字节数组时,encoding 属性第1个字节的前2位后的字节标识了字节数组的实际长度
  • 当 content 存储的是整数时,encoding 属性第1个字节的3、4位标识了整数的类型。当 encoding 属性标识元素存储的是区间 (0,12] 的整数时,整数内容直接存储在 encoding 属性中,此时没有 content 属性

Redis 中定义了以下常量标识 encoding 属性的编码类型:

  • // ziplist.c
    #define ZIP_STR_06B (0 << 6)			 // 00000000
    #define ZIP_STR_14B (1 << 6)       // 01000000
    #define ZIP_STR_32B (2 << 6)       // 10000000
    #define ZIP_INT_16B (0xc0 | 0<<4)  // 11000000 = 11000000 | 00000000
    #define ZIP_INT_32B (0xc0 | 1<<4)  // 11010000 = 11000000 | 00010000
    #define ZIP_INT_64B (0xc0 | 2<<4)  // 11100000 = 11000000 | 00100000
    #define ZIP_INT_24B (0xc0 | 3<<4)  // 11110000 = 11000000 | 00110000
    #define ZIP_INT_8B 0xfe            // 11111110
    

3. 结构体

3.1 元素编码缓存

压缩列表元素编码结构复杂,当解码后获取到元素的前一个元素的长度、元素存储的数据类型、元素的数据内容时,可以将这些属性缓存到结构体 zlentry

  • // ziplist.c
    typedef struct zlentry {
        unsigned int prevrawlensize; 
        unsigned int prevrawlen;     
        unsigned int lensize;       
        unsigned int len;           
        unsigned int headersize;     
        unsigned char encoding;      
        unsigned char *p;            
    } zlentry;
    
  • prevrawlensize:previous_entry_length 属性的字节长度

  • prevrawlen:前一个元素的字节长度

  • lensize:encoding 属性的字节长度

  • len:元素数据内容的字节长度

  • headersize:元素头部的字节长度(prevrawlensize+lensize)

  • encoding:元素数据内容的类型

  • p:元素的首地址

3.2 元素解码

  • // ziplist.c
    void zipEntry(unsigned char *p, zlentry *e) {
        // 解析 prevrawlensize、prevrawlen
        ZIP_DECODE_PREVLEN(p, e->prevrawlensize, e->prevrawlen);
        // 解析 encoding、lensize、len
        ZIP_DECODE_LENGTH(p + e->prevrawlensize, e->encoding, e->lensize, e->len);
        // 计算 headersize
        e->headersize = e->prevrawlensize + e->lensize;
        // 设置 p
        e->p = p;
    }
    
  • // ziplist.c
    // ptr 指向元素首地址,即 previous_entry_length 属性的首地址
    #define ZIP_DECODE_PREVLEN(ptr, prevlensize, prevlen) do { 
        // 解析 prevlensize
        ZIP_DECODE_PREVLENSIZE(ptr, prevlensize);                                  
        if ((prevlensize) == 1) {
            // 如果 previous_entry_length 属性占1个字节
            // 则根据该字节解析出 prevlen
            (prevlen) = (ptr)[0];                                                  
        } else if ((prevlensize) == 5) {
            // 如果 previous_entry_length 属性占5个字节
            // 则根据第2~5个字节解析出 prevlen
            assert(sizeof((prevlen)) == 4);                                    
            memcpy(&(prevlen), ((char*)(ptr)) + 1, 4);                             
            memrev32ifbe(&prevlen);                                                
        }                                                                          
    } while(0);
    
    #define ZIP_BIG_PREVLEN 254
    
    #define ZIP_DECODE_PREVLENSIZE(ptr, prevlensize) do {                          
        if ((ptr)[0] < ZIP_BIG_PREVLEN) {
            // 如果前一个元素字节长度小于254
            // 则 previous_entry_length 属性占1个字节,prevlensize=1
            (prevlensize) = 1;                                                     
        } else {
            // 如果前一个元素字节长度大于等于254
            // 则 previous_entry_length 属性占5个字节,prevlensize=5
            (prevlensize) = 5;                                                     
        }                                                                          
    } while(0);
    
  • // ziplist.c
    #define ZIP_STR_MASK 0xc0 // 11000000
    
    // ptr 指向元素 encoding 属性的首地址
    #define ZIP_DECODE_LENGTH(ptr, encoding, lensize, len) do {
        // 解析 encoding
        ZIP_ENTRY_ENCODING((ptr), (encoding));                                     
        if ((encoding) < ZIP_STR_MASK) { 
            // 元素为字节数组时
            // 根据第一个字节前两位区分出编码类型(最大长度)
            // 然后进一步解析出 lensize、len
            if ((encoding) == ZIP_STR_06B) { 
                (lensize) = 1;                                                     
                (len) = (ptr)[0] & 0x3f;                                           
            } else if ((encoding) == ZIP_STR_14B) {                                
                (lensize) = 2;                                                     
                (len) = (((ptr)[0] & 0x3f) << 8) | (ptr)[1];                       
            } else if ((encoding) == ZIP_STR_32B) {                                
                (lensize) = 5;                                                     
                (len) = ((ptr)[1] << 24) |                                         
                        ((ptr)[2] << 16) |                                         
                        ((ptr)[3] <<  8) |                                         
                        ((ptr)[4]);                                                
            } else {                                                               
                panic("Invalid string encoding 0x%02X", (encoding));               
            }                                                                      
        } else { 
            // 元素为整数时,encoding 长度为1字节
            (lensize) = 1;
            // 解析整数长度 len
            (len) = zipIntSize(encoding);                                          
        }                                                                          
    } while(0);
    
    #define ZIP_ENTRY_ENCODING(ptr, encoding) do {  
        (encoding) = (ptr[0]);
        // 处理 encoding 属性第1个字节:
        //     1. ptr[0] < ZIP_STR_MASK,说明为字节数组,根据前2个比特可知字节数组编码类型
        //     2. ptr[0] >= ZIP_STR_MASK ,说明为整数,不处理
        if ((encoding) < ZIP_STR_MASK) (encoding) &= ZIP_STR_MASK; 
    } while(0);
    
    unsigned int zipIntSize(unsigned char encoding) {
        // 对比 encoding 属性的编码类型,解析出 len
        switch(encoding) {
        case ZIP_INT_8B:  return 1;
        case ZIP_INT_16B: return 2;
        case ZIP_INT_24B: return 3;
        case ZIP_INT_32B: return 4;
        case ZIP_INT_64B: return 8;
        }
        // 区间 (0,12] 的整数,content 属性不存在,len=0
        if (encoding >= ZIP_INT_IMM_MIN && encoding <= ZIP_INT_IMM_MAX)
            return 0;
        panic("Invalid integer encoding 0x%02X", encoding);
        return 0;
    }
    

4. 基本操作

4.1 创建压缩链表

// ziplist.c
unsigned char *ziplistNew(void) {
    // 初始内存空间大小 = 压缩列表头部+尾部 = zlbytes+zltail+zllen+zlend = 4+4+2+1 = 11
    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;
    // 设置尾部标识0xFF
    zl[bytes-1] = ZIP_END;
    return zl;
}

4.2 插入元素

压缩列表插入元素步骤:

  • 元素编码
  • 重新分配空间
  • 数据复制
4.2.1 编码

编码即解析元素的 previous_entry_length、encoding、content 属性

4.2.1.1 插入元素位置的情况

插入元素位置

  • p0:压缩列表没有任何元素时,不存在前一个元素,即前一个元素的字节长度为0
  • p1:在压缩列表已存在元素的中间插入时,需要获取 entryX 元素的字节长度,而 entryX+1 元素的 previous_entry_length 属性存储了 entryX 元素的字节长度,可以直接获取
  • p2:在压缩列表的尾部元素后插入时,需要获取 entryN 元素的字节长度,此时需要解析出 entryN 元素的 prevrawlensize、lensize、len 后相加得到长度
4.2.1.2 解析元素属性
// ziplist.c
// zl:指向压缩列表首地址
// p:指向元素插入位置
// s:指向插入的数据内容
// slen:表示数据内容的字节长度
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;
    zlentry tail;

    if (p[0] != ZIP_END) {
        // 插入位置是压缩列表已存在元素的中间
        // 调用 ZIP_DECODE_PREVLEN() 解析出 prevlen
        ZIP_DECODE_PREVLEN(p, prevlensize, prevlen);
    } else {
        // 插入位置是压缩列表的尾部
        unsigned char *ptail = ZIPLIST_ENTRY_TAIL(zl);
        // 尾元素存在时,调用 zipRawEntryLength() 解析出 prevlen
        // 尾元素不存在时,没有前一个元素,prevlen=0 
        if (ptail[0] != ZIP_END) {
            prevlen = zipRawEntryLength(ptail);
        }
    }

    // 尝试按照整数解析数据内容 s
    // 成功时,根据 encoding 进一步解析整数 s 的字节长度
    // 失败时,s 为字节数组,字节长度为 slen
    if (zipTryEncoding(s,slen,&value,&encoding)) {
        reqlen = zipIntSize(encoding);
    } else {
        reqlen = slen;
    }
    
    // 获取 previous_entry_length、encoding 属性的字节长度,并累加到 reqlen
    reqlen += zipStorePrevEntryLength(NULL,prevlen);
    reqlen += zipStoreEntryEncoding(NULL,encoding,slen);

    // 此时已解析出插入元素的字节长度
    // 以及三个属性的字节长度和值
    // ......
}
4.2.1.3 按照整数解析
// ziplist.c
int zipTryEncoding(unsigned char *entry, unsigned int entrylen, long long *v, unsigned char *encoding) {
    long long value;

    // 如果数据内容字节长度等于0或大于等于32,则返回0
    if (entrylen >= 32 || entrylen == 0) return 0;
    if (string2ll((char*)entry,entrylen,&value)) {
        // 数据内容解析为 long long 类型的整数 value
        // 进一步解析出 encoding 属性
        if (value >= 0 && value <= 12) {
            *encoding = ZIP_INT_IMM_MIN+value;
        } else if (value >= INT8_MIN && value <= INT8_MAX) {
            *encoding = ZIP_INT_8B;
        } else if (value >= INT16_MIN && value <= INT16_MAX) {
            *encoding = ZIP_INT_16B;
        } else if (value >= INT24_MIN && value <= INT24_MAX) {
            *encoding = ZIP_INT_24B;
        } else if (value >= INT32_MIN && value <= INT32_MAX) {
            *encoding = ZIP_INT_32B;
        } else {
            *encoding = ZIP_INT_64B;
        }
        // 将 value 的值传递给调用方
        *v = value;
        return 1;
    }
    return 0;
}
4.2.1.4 获取 previous_entry_length 属性的字节长度
// ziplist.c
unsigned int zipStorePrevEntryLength(unsigned char *p, unsigned int len) {
    
    if (p == NULL) {
        // p == NULL 时,直接返回 previous_entry_length 属性占用的字节长度
        // len < 254 时,previous_entry_length 属性占用1个字节
        // len >= 254 时,previous_entry_length 属性占用5个字节
        return (len < ZIP_BIG_PREVLEN) ? 1 : sizeof(len)+1;
    } else {
        // p != NULL 时,将 len 存储到 previous_entry_length 属性中
        if (len < ZIP_BIG_PREVLEN) {
            // previous_entry_length 属性占用1个字节时
            // 第1个字节存储 len
            p[0] = len;
            return 1;
        } else {
            return zipStorePrevEntryLengthLarge(p,len);
        }
    }
}

int zipStorePrevEntryLengthLarge(unsigned char *p, unsigned int len) {
    if (p != NULL) {
        // previous_entry_length 属性占用5个字节时
        // 第1个字节固定为0xFE
        // 后4个字节存储长度 len
        p[0] = ZIP_BIG_PREVLEN;
        memcpy(p+1,&len,sizeof(len));
        memrev32ifbe(p+1);
    }
    return 1+sizeof(len);
}
4.2.1.5 获取 encoding 属性的字节长度
// ziplist.c
unsigned int zipStoreEntryEncoding(unsigned char *p, unsigned char encoding, unsigned int rawlen) {
    unsigned char len = 1, buf[5];

    if (ZIP_IS_STR(encoding)) {
        // 数据类型为字节数组
        // p=NULL 时,直接返回 encoding 属性占用的字节长度
        // p!=NULL 时,将字节数组的类型和长度存储到 encoding 属性中
        if (rawlen <= 0x3f) {
            // 字节数组长度小于等于63,encoding 属性占用1个字节
            if (!p) return len;
            buf[0] = ZIP_STR_06B | rawlen;
        } else if (rawlen <= 0x3fff) {
            // 字节数组长度区间(63,2^14-1],encoding 属性占用2个字节
            len += 1;
            if (!p) return len;
            buf[0] = ZIP_STR_14B | ((rawlen >> 8) & 0x3f);
            buf[1] = rawlen & 0xff;
        } else {
            // 字节数组长度区间(2^14-1,2^32-1],encoding 属性占用5个字节
            len += 4;
            if (!p) return len;
            buf[0] = ZIP_STR_32B;
            buf[1] = (rawlen >> 24) & 0xff;
            buf[2] = (rawlen >> 16) & 0xff;
            buf[3] = (rawlen >> 8) & 0xff;
            buf[4] = rawlen & 0xff;
        }
    } else {
        // 数据类型为整数
        // p=NULL 时,则直接返回 encoding 属性所需的字节长度
        // p!=NULL 时,将整数的类型存储到 encoding 属性中
        if (!p) return len;
        buf[0] = encoding;
    }

    memcpy(p,buf,len);
    return len;
}
4.2.2 重新分配空间

插入新元素时,压缩列表的空间会变大,因此需要重新分配空间

新空间大小=旧空间大小+新元素空间大小,该等式不完全成立。假设插入元素前,entryX 元素长度为128个字节,entryX+1 元素的 previous_entry_length 属性占1个字节。添加长度为1024个字节的 entryNew 元素时,entryX+1 元素的 previous_entry_length 属性需要占5个字节,即压缩链表的长度不仅增加了1024个字节,还要加上 entryX+1 元素扩展的4个字节。 entryX+1 元素长度可能增加4个字节,减少4个字节或不变

压缩列表长度变化

// ziplist.c
unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {
    // ......

    int forcelarge = 0;
    // nextdiff 表示插入新元素时,其下个元素字节长度的变化,可能为0、-4、+4
    // 当插入位置在压缩列表尾部时,不存在下个元素,nextdiff=0
    // 否则进一步计算 nextdiff
    nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0;
    if (nextdiff == -4 && reqlen < 4) {
        // nextdiff = -4 且 reqlen < 4 时,压缩链表插入新元素导致空间变小了
        // 重新分配空间可能导致数据丢失(此时还没数据复制)
        // 为避免这种情况,重新赋值 nextdiff=0 ,同时进行标记 forcelarge=1 
        // 上述情况一般不会发生,只有在连锁更新后可能存在
        nextdiff = 0;
        forcelarge = 1;
    }

    // 重新分配空间,可能导致插入位置 p 失效
    // 因此记录指针 p 相对压缩列表首地址 zl 的偏移量 offset
    offset = p-zl;
    // 重新分配空间
    zl = ziplistResize(zl,curlen+reqlen+nextdiff);
    // 重新计算插入位置 p 
    p = zl+offset;

    // ......
}

int zipPrevLenByteDiff(unsigned char *p, unsigned int len) {
    unsigned int prevlensize;
    // 计算 p 指向元素的 previous_entry_length 属性的字节长度 prevlensize
    ZIP_DECODE_PREVLENSIZE(p, prevlensize);
    // 计算字节长度 len 对应的 previous_entry_length 属性的字节长度
    // 然后与 prevlensize 相减
    return zipStorePrevEntryLength(NULL, len) - prevlensize;
}
4.2.3 数据复制

重新分配内存后,需要将插入位置 p 后的元素移动到指定位置,腾出待插入元素的内存空间,然后再插入元素

压缩列表数据复制

// ziplist.c
unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {
    // ......
  
    if (p[0] != ZIP_END) {
        // 如果不是尾部插入,则需要移动插入位置 p 后的元素
      
        // 复制 p 后的所有元素到新位置
        // 数据块长度为 p 后所有元素的总字节长度加上 nextdiff
        memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff);

        // 更新待插入元素的后一个元素的 previous_entry_length 属性
        if (forcelarge)
            zipStorePrevEntryLengthLarge(p+reqlen,reqlen);
        else
            zipStorePrevEntryLength(p+reqlen,reqlen);

        // 更新 zltail
        ZIPLIST_TAIL_OFFSET(zl) =
            intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+reqlen);

        // 将待插入元素后一个元素的解码为 tail
        zipEntry(p+reqlen, &tail);
        if (p[reqlen+tail.headersize+tail.len] != ZIP_END) {
            // 如果待插入元素的后一个元素不是尾元素,则 zltail 还需要偏移 nextdiff
            ZIPLIST_TAIL_OFFSET(zl) =
                intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+nextdiff);
        }
    } else {
        // 如果尾部插入,则插入位置 p (待插入元素首地址) 到压缩列表首地址的偏移量为 zltail
        ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(p-zl);
    }

    if (nextdiff != 0) {
        // 如果 nextdiff不为0,则此时压缩列表可能需要连续更新
        // 从而导致压缩列表首地址和插入位置 p 失效,所以可能需要重新赋值
        offset = p-zl;
        zl = __ziplistCascadeUpdate(zl,p+reqlen);
        p = zl+offset;
    }

    // 将待插入元素的 previous_entry_length、encoding、content 编码后存储在插入位置 p
    p += zipStorePrevEntryLength(p,prevlen);
    p += zipStoreEntryEncoding(p,encoding,slen);
    if (ZIP_IS_STR(encoding)) {
        memcpy(p,s,slen);
    } else {
        zipSaveInteger(p,value,encoding);
    }
    // zllen 加1
    ZIPLIST_INCR_LENGTH(zl,1);
    return zl;
}

4.3 删除元素

压缩列表删除元素的步骤:

  • 计算待删除元素的总长度
  • 数据复制
  • 重新分配内存
4.3.1 计算待删除元素的总长度
// ziplist.c
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;

    // 解码第1个待删除元素 first
    zipEntry(p, &first);
    // 遍历所有待删除元素,指针 p 移动到最后一个待删除元素的尾地址
    // 同时对待删除元素计数 deleted
    for (i = 0; p[0] != ZIP_END && i < num; i++) {
        p += zipRawEntryLength(p);
        deleted++;
    }

    // 计算待删除元素的总长度
    totlen = p-first.p;
    
    // ......
}
4.3.2 数据复制

新空间大小=旧空间大小-待删除元素的总长度,该等式不完全成立。假设删除元素 entryX+1 到元素 entryN-1 之间的元素,元素 entryN-1 长度为12个字节,所以元素 entryN 的 previous_entry_length 属性占用1个字节。删除元素后,长度为512个字节的元素 entryX 成为元素 entryN 的前一个元素,此时元素 entryN 的 previous_entry_length 属性需要占用5个字节,即删除元素之后的压缩列表的总长度还与元素 entryN 长度有关

压缩列表删除元素

// ziplist.c
unsigned char *__ziplistDelete(unsigned char *zl, unsigned char *p, unsigned int num) {
    // ......
  
    if (totlen > 0) {
        if (p[0] != ZIP_END) {
            // 如果待删除元素删除后,p 指向元素存在

            // 计算 p 指向元素字节长度的变化 nextdiff
            nextdiff = zipPrevLenByteDiff(p,first.prevrawlen);

            // 根据 nextdiff 移动 p
            p -= nextdiff;
            // 更新 p 指向元素的 previous_entry_length 属性存储的前一个元素的字节长度
            zipStorePrevEntryLength(p,first.prevrawlen);

            // 根据 totlen 更新 zltail
            ZIPLIST_TAIL_OFFSET(zl) =
                intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))-totlen);

            // 解码 p 指向元素 tail
            zipEntry(p, &tail);
            if (p[tail.headersize+tail.len] != ZIP_END) {
                // 元素 tail 后还存在元素时,更新 zltail
                ZIPLIST_TAIL_OFFSET(zl) =
                   intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+nextdiff);
            }

            // 复制 p 后的所有元素到新位置
            // 数据块长度为 p 后所有元素的总字节长度
            memmove(first.p,p,
                intrev32ifbe(ZIPLIST_BYTES(zl))-(p-zl)-1);
        } else {
            // 如果待删除元素删除后,p 指向元素不存在
            // 直接计算出 zltail ,无需数据复制
            ZIPLIST_TAIL_OFFSET(zl) =
                intrev32ifbe((first.p-zl)-first.prevrawlen);
        }

        // ......
    }
  
    // ......
}
4.3.3 重新分配内存
// ziplist.c
unsigned char *__ziplistDelete(unsigned char *zl, unsigned char *p, unsigned int num) {
    // ......
  
    if (totlen > 0) {
        // ......

        // 重新分配内存时,导致压缩列表首地址和指针 p 失效
        // 需要重新赋值
        offset = first.p-zl;
        zl = ziplistResize(zl, intrev32ifbe(ZIPLIST_BYTES(zl))-totlen+nextdiff);
        // 更新 zllen
        ZIPLIST_INCR_LENGTH(zl,-deleted);
        p = zl+offset;

        if (nextdiff != 0)
            // nextdiff!=0 时,p 指向元素的字节长度发生变化
            // 可能导致后面的元素需要连锁更新
            zl = __ziplistCascadeUpdate(zl,p);
    }
    return zl;
}

4.4 遍历压缩列表

4.4.1 前向遍历
// ziplist.c
unsigned char *ziplistNext(unsigned char *zl, unsigned char *p) {
    ((void) zl);

    
    if (p[0] == ZIP_END) {
        // p 指向 zlend 时,没有元素可遍历了
        return NULL;
    }

    // 获取 p 指向元素的字节长度,移动该长度后指向下一个元素首地址
    p += zipRawEntryLength(p);
    if (p[0] == ZIP_END) {
        return NULL;
    }

    return p;
}
4.4.2 后向遍历
// ziplist.c
unsigned char *ziplistPrev(unsigned char *zl, unsigned char *p) {
    unsigned int prevlensize, prevlen = 0;

    if (p[0] == ZIP_END) {
        // p 指向 zlend 时,将 p 重新指向尾元素
        // 尾元素存在时返回
        // 尾元素不存在时,表示没有元素可遍历了
        p = ZIPLIST_ENTRY_TAIL(zl);
        return (p[0] == ZIP_END) ? NULL : p;
    } else if (p == ZIPLIST_ENTRY_HEAD(zl)) {
        // p 指向头元素时,没有元素可遍历了
        return NULL;
    } else {
        // 根据 p 指向元素的 previous_entry_length 属性存储的前一个元素的字节长度
        // 将 p 重新指向前一个元素的首地址后返回
        ZIP_DECODE_PREVLEN(p, prevlensize, prevlen);
        assert(prevlen > 0);
        return p-prevlen;
    }
}

4.5 连锁更新

在插入或删除元素时,除了操作位置的后一个元素的字节长度可能发生变化,还可能导致后续其他元素的字节长度也发生变化

  • 压缩列表 zl1 在位置 p1 删除元素 extryX 时,元素 entryX+1 的前一个元素变成元素 entryX-1 ,前一个元素的长度从128字节变成512字节,元素 entryX+1 的长度需要从253字节扩展到257字节(前一个元素字节长度大于等于254字节时,previous_entry_length 属性字节长度为5字节),导致元素 entryX+2 也需要从253字节扩展到257字节,依次类推,发生连锁更新
  • 压缩列表 zl2 在位置 p2 插入元素 extryY 时,与上面删除类似,前一个元素的字节长度变大,导致后面元素的字节长度也变大,依次类推,发生连锁更新
  • 当插入或删除元素,前一个元素的字节长度变小,导致后面元素的字节长度可以变小时,Redis 实际未做处理

压缩列表连锁更新

// ziplist.c
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;

    // p 指向元素存在时,执行连锁更新逻辑
    while (p[0] != ZIP_END) {
        // 解码 p 指向元素 cur
        zipEntry(p, &cur);
        // 获取元素 cur 的字节长度 rawlen
        rawlen = cur.headersize + cur.len;
        // 获取元素 cur 的 previous_entry_length 属性的字节长度 rawlensize
        rawlensize = zipStorePrevEntryLength(NULL,rawlen);

        // 如果元素 cur 的下一个元素不存在,则无需执行连锁更新逻辑
        if (p[rawlen] == ZIP_END) break;
        // p+rawlen 指向下一个元素,解码元素 next
        zipEntry(p+rawlen, &next);

        // 如果元素 next 的 previous_entry_length 属性存储的前一个元素的字节长度没有发生变化
        // 则无需执行连锁更新逻辑
        if (next.prevrawlen == rawlen) break;

        if (next.prevrawlensize < rawlensize) {
            // 如果元素 next 的 previous_entry_length 属性的字节长度变大了
          
            // 重新分配内存时,导致压缩列表首地址和指针 p 失效
            // 需要重新赋值
            offset = p-zl;
            // 计算元素 next 的 previous_entry_length 属性需要扩展的字节长度 extra
            extra = rawlensize-next.prevrawlensize;
            zl = ziplistResize(zl,curlen+extra);
            p = zl+offset;

            // np 指向元素 next 的首地址 
            np = p+rawlen;
            // 元素 next 的首地址相对于压缩列表首地址的偏移量 noffset
            noffset = np-zl;

            if ((zl+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))) != np) {
                // 如果元素 next 不是压缩列表的尾元素
                // 则需要更新 zltail
                ZIPLIST_TAIL_OFFSET(zl) =
                        intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+extra);
            }

            // 复制元素 next 的 encoding 属性首地址后的数据到新地址
            memmove(np+rawlensize,
                    np+next.prevrawlensize,
                    curlen-noffset-next.prevrawlensize-1);
            // 存储元素 cur 的字节长度 rawlen 到元素 next 的 previous_entry_length 属性中
            zipStorePrevEntryLength(np,rawlen);

            // 移动指针 p 指向下一个需要执行连锁更新逻辑的元素
            p += rawlen;
            // 更新压缩列表字节长度 curlen
            curlen += extra;
        } else {
            if (next.prevrawlensize > rawlensize) {
                // 如果元素 next 的 previous_entry_length 属性的字节长度可以变小
                // 则实际不执行执行连锁更新逻辑
                // 只更新元素 next 的 previous_entry_length 属性存储的值(元素 cur 的字节长度)
                zipStorePrevEntryLengthLarge(p+rawlen,rawlen);
            } else {
                // 如果元素 next 的 previous_entry_length 属性的字节长度不变
                // 则不执行执行连锁更新逻辑
                // 只更新元素 next 的 previous_entry_length 属性存储的值(元素 cur 的字节长度)
                zipStorePrevEntryLength(p+rawlen,rawlen);
            }

            break;
        }
    }
    return zl;
}

压缩列表连锁更新实现逻辑

学自《Redis 5设计与源码分析》