redis源码分析与思考(七)——压缩列表

    ziplist,即压缩列表。在Redis中,它是为了尽量节约内存而开发出来的,可谓是将节约内存做到了极致。它是哈希键与列表键的底层实现之一,当哈希键与列表键只含有整数或者两者的键长度较小时,两者就采用压缩列表来实现其功能。

    之所以被称作压缩列表,是因为不同于之前所说的adlist表,压缩列表将普通顺序表的pre与next指针取消掉,采用计算长度的方法来实现各种操作,而且其长度也做了动态内存分配。 打个比方,在存贮内容时你只用了一两个字节,而pre与next指针却要占到几个字节,显然是不合理的。压缩列表就是把这个用计算长度的方法来代替,在一定的程度上减少了大量的内存,也就起到了压缩的作用。
    当你在阅读其源码时,你会发现压缩列表本质上是一个字符串,只不过这个字符串由一系列的连续内存地址块组成。 一个压缩列表包含多个节点(entry),而一个节点包含着一个字节数组或者一个整数。

结构与创建

     压缩列表的创建是直接基于指针操作的。它申请一块内存,并将这块内存分成几块分别用于记录该压缩列表的所占空间、多少节点等等,从而使得压缩列表看起来像是由一系列连续地址块组成的顺序表结构:

#define ZIP_END 255
// 定位到 ziplist 的 bytes 属性,该属性记录了整个 ziplist 所占用的内存字节数
// 用于取出 bytes 属性的现有值,或者为 bytes 属性赋予新值
#define ZIPLIST_BYTES(zl)       (*((uint32_t*)(zl)))
// 定位到 ziplist 的 offset 属性,该属性记录了到达表尾节点的偏移量
// 用于取出 offset 属性的现有值,或者为 offset 属性赋予新值
#define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t))))
// 定位到 ziplist 的 length 属性,该属性记录了 ziplist 包含的节点数量
// 用于取出 length 属性的现有值,或者为 length 属性赋予新值
#define ZIPLIST_LENGTH(zl)      (*((uint16_t*)((zl)+sizeof(uint32_t)*2)))
// 返回 ziplist 表头的大小
#define ZIPLIST_HEADER_SIZE     (sizeof(uint32_t)*2+sizeof(uint16_t))
// 返回指向 ziplist 第一个节点(的起始位置)的指针
#define ZIPLIST_ENTRY_HEAD(zl)  ((zl)+ZIPLIST_HEADER_SIZE)
// 返回指向 ziplist 最后一个节点(的起始位置)的指针
#define ZIPLIST_ENTRY_TAIL(zl)  ((zl)+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl)))
// 返回指向 ziplist 末端 ZIP_END (的起始位置)的指针
#define ZIPLIST_ENTRY_END(zl)   ((zl)+intrev32ifbe(ZIPLIST_BYTES(zl))-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;
    return zl;
}

    大量的使用宏定义来完成指针的移位操作,并且在指针移位的时候通过强制转换将指定的地址转换为存贮目标信息的位置。通过对宏定义的分析,不难得到压缩列表第一部分是ZIPLIST_BYTES,也就是首地址所在,它是用来存贮整个压缩列表所占字节的多少,即表示着压缩列表的大小,而其后是ZIPLIST_TAIL_OFFSET,这一部分的地址是通过第一部分移位sizeof(uint32_t)个字节大小得到的,从而得出第一部分占有sizeof(uint32_t)个字节,即4个字节,同理,可以通过该方法得到其余的几大部分:

1. ZIPLIST_BYTES:这部分是用来存贮压缩列表所占空间的大小,占4个字节。
2. ZIPLIST_TAIL_OFFSET:这部分是用来存贮偏移量的,这个偏移量用来计算最后一个节点的位置,所以压缩列表得到最后一个节点的位置复杂度为O(1),占4个字节。
3. ZIPLIST_LENGTH:这一部分用来记录包含节点的数量,占两个字节。而第一、二、三部分构成了压缩列表的头。
4. ZIPLIST_ENTRY_HEAD:该指针指向第一个节点。
5. ZIPLIST_ENTRY_TAIL:该指针指向最后一个节点。第四以及第五指向的节点加上中间的节点共同组成压缩列表的body部分,即内容。
6. ZIPLIST_ENTRY_END:这一指针指向压缩列表的尾。
7. ZIP_END:这一部分表示压缩列表的尾部,用来标示已到尽头,占1字节。

    如图所示:
在这里插入图片描述

节点的结构

    压缩列表的结构并没有在代码中明确的定义,而是采用了动态申请内存的方法来完成节点的创建与构造,与压缩列表的其余几大模块一样,在需要时被构造,节点的结构如下:
在这里插入图片描述

pre_length

    pre_length是用来记录上一个节点所占空间的大小的,大小是以字节数的多少来计算的,该属性长度为1个字节或者是5个字节。当前一个节点大小小于ZIP_BIGLEN字节时,该属性就采用1个字节来表示上一个节点的空间长度,而大于它时,就采用5个字节来描述:

#define ZIP_BIGLEN 254
//对pre_length进行写入并返回其空间长度
static unsigned 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);
        }
    }
}

    代码中注意的是,p[0]不仅仅表示p指向压缩列表的节点,同时也指向该节点的第一部分,即pre_length。而且在处理5字节长的数据时,采用memcpy复制len中的数据。pre_length开头字节等于ZIP_BIGLEN时,表示这是一个5字节的pre_length属性,5字节长度pre_length计算前一节点的空间长度方式是除掉最高字节,计算剩余的字节就可得到其长度。即如下:


编码形式编码长度
11111110 ******** ******** ******** ********5字节
******** (不包括11111111与11111110)1字节

    pre_length不仅表示着前一个节点的空间长度,同时也可以用它来推导上一个节点的位置,即当前节点的首地址减去pre_length的大小就等于上一个节点的首地址了,这是压缩列表从尾部向头部遍历的原理。

encoding、content

    压缩列表中存贮的有两种数据,一种是字符串,另外一种则是整数。而压缩列表为了节省空间,采用分治的思想来采用不同的编码格式,即不同的数据大小来采用不同的编码格式。同时,该属性不仅表示着编码格式,还表示着该节点内容的空间长度。该属性长度为1个字节、2个字节或者5个字节。压缩列表中主要数据类型有如下:

/*
 * 字符串编码类型
 */
#define ZIP_STR_06B (0 << 6) ----> (2^6-1)字节字符串
#define ZIP_STR_14B (1 << 6) ----> (2^14-1)字节字符串
#define ZIP_STR_32B (2 << 6) ----> (2^32-1)32字节字符串
/*
 * 整数编码类型
 */
#define ZIP_INT_16B (0xc0 | 0<<4) ----> 2字节整数
#define ZIP_INT_32B (0xc0 | 1<<4) ----> 4字节整数
#define ZIP_INT_64B (0xc0 | 2<<4) ----> 8字节整数
#define ZIP_INT_24B (0xc0 | 3<<4) ----> 3字节整数
#define ZIP_INT_8B 0xfe ----> 1字节整数
/* 
 * 4 位整数编码的掩码和类型
 */
#define ZIP_INT_IMM_MASK 0x0f ----> 4位整数
#define ZIP_INT_IMM_MIN 0xf1    /* 11110001 */
#define ZIP_INT_IMM_MAX 0xfd    /* 11111101 */

    写入创建endoding编码代码如下:

#define ZIP_STR_MASK 0xc0
//判断是否是字符串编码格式
#define ZIP_IS_STR(enc) (((enc) & ZIP_STR_MASK) < ZIP_STR_MASK)
//对encoding的进行写入并返回所占空间长度
static unsigned int zipEncodeLength(unsigned char *p, unsigned char encoding, unsigned int rawlen) {
    unsigned char len = 1, buf[5];
    // 编码字符串
    if (ZIP_IS_STR(encoding)) {
    //长度小于2^6-1
        if (rawlen <= 0x3f) {
            if (!p) return len;
            buf[0] = ZIP_STR_06B | rawlen;
            //长度小于2^14-1
        } else if (rawlen <= 0x3fff) {
            len += 1;
            if (!p) return len;
            buf[0] = ZIP_STR_14B | ((rawlen >> 8) & 0x3f);
            buf[1] = rawlen & 0xff;
            //长度小于2^32-1
        } else {
            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 {
        if (!p) return len;
        buf[0] = encoding;
    }
    // 将编码后的长度写入 p 
    memcpy(p,buf,len);
    // 返回编码所需的字节数
    return len;
}

    之所以宏定义ZIP_IS_STR可以判断是否是字符串编码,是因为整数编码与字符串编码有个特色,即整数编码化为二进制表示开头都为11******,而字符串编码为00******,01******,10******开头,所以取与操作后,字符串的编码都比ZIP_STR_MASK要小。

    对上述代码分析得出,整数编码与字符串编码有如下的格式:

字符串编码

编码格式编码的大小content保存的值
00xxxxxx1字节小于等于26-1
01xxxxxx   xxxxxxxx2字节小于等于214-1
10____ xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx5字节小于等于232-1

整数编码

编码格式编码的大小content保存的值
110000001字节int_16t整数
110100001字节int_32t整数
111000001字节int_64t整数
111100001字节24位无符号整数
111111101字节8位无符号整数
1111xxxx1字节4位整数,该编码格式没有对应的content,因为它只表示0到12的数字,而其中编码格式里xxxx就可以代替content表示

    对于整数类型来说,除掉最高的两位,剩余的便表示节点是哪种类型,而对于字符串数组来说最高两位表示其是字符串数组,剩余的便表示节点包含字符串的长度大小。 例如对于字符串数组有如下编码格式:00000001。其表示一字节的含有长度为一的字符串。

节点的取值

    在压缩列表中,专门定义了一个结构体来保存每个节点所包含的属性值,该结构体不是节点的定义,在阅读源码时很容易混淆,需特别的注意:

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;

    先前说过,压缩列表本质上是一个无符号字符数组,由一系列连续的地址块组成,所以当需要取出目标地址所存数据或者存入数据时,都需要对其进行解码操作,即将其转化为目标地址所存数据的数据类型。首先从取出pre_length与encoding属性的值说起:

//取出目标节点的pre_length属性所占空间大小
#define ZIP_DECODE_PREVLENSIZE(ptr, prevlensize) do {                          \
    if ((ptr)[0] < ZIP_BIGLEN) {                                               \
        (prevlensize) = 1;                                                     \
    } else {                                                                   \
        (prevlensize) = 5;                                                     \
    }                                                                          \
} while(0);

//计算目标节点上一节点的所占空间大小
#define ZIP_DECODE_PREVLEN(ptr, prevlensize, prevlen) do {                     \                                                                           \
    /* 先计算被编码长度值的字节数 */                                           \
    ZIP_DECODE_PREVLENSIZE(ptr, prevlensize);                                  \                                                                          \
    /* 再根据编码字节数来取出长度值 */                                         \
    if ((prevlensize) == 1) {                                                  \
        (prevlen) = (ptr)[0];                                                  \
    } else if ((prevlensize) == 5) {     \
        assert(sizeof((prevlensize)) == 4); /**/                                   \
        memcpy(&(prevlen), ((char*)(ptr)) + 1, 4);                             \
        memrev32ifbe(&prevlen);                                                \
    }                                                                          \
} while(0);

//取出目标节点的encoding编码值
#define ZIP_ENTRY_ENCODING(ptr, encoding) do {  \
    (encoding) = (ptr[0]); \
    if ((encoding) < ZIP_STR_MASK) (encoding) &= ZIP_STR_MASK; \
} while(0)

//取出目标节点encoding所占空间的大小
#define ZIP_DECODE_LENGTH(ptr, encoding, lensize, len) do {                    
    /* 取出值的编码类型 */                                                     
    ZIP_ENTRY_ENCODING((ptr), (encoding));                                     
    /* 字符串编码 */                                                           
    if ((encoding) < ZIP_STR_MASK) {                                           
        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 {                                                               
            assert(NULL);                                                      
        }                                                                      
    /* 整数编码 */                                                             
    } else {                                                                   
        (lensize) = 1;                                                         
        (len) = zipIntSize(encoding);                                          
    } 
} while(0);

//将目标节点的值保存在zlentry结构体中
static zlentry zipEntry(unsigned char *p) {
    zlentry e;
    // e.prevrawlensize 保存着编码前一个节点的长度所需的字节数
    // e.prevrawlen 保存着前一个节点的长度
    ZIP_DECODE_PREVLEN(p, e.prevrawlensize, e.prevrawlen);
    // p + e.prevrawlensize 将指针移动到列表节点本身
    // e.encoding 保存着节点值的编码类型
    // e.lensize 保存着编码节点值长度所需的字节数
    // e.len 保存着节点值的长度
    // T = O(1)
    ZIP_DECODE_LENGTH(p + e.prevrawlensize, e.encoding, e.lensize, e.len);
    // 计算头结点的字节数
    e.headersize = e.prevrawlensize + e.lensize;
    // 记录指针
    e.p = p;
    return e;
}

    对encoding与pre_length的一些常用操作:

 //将原本只需要 1 个字节来保存的前置节点长度 len 编码至一个 5 字节长的 header 中。
static void zipPrevEncodeLengthForceLarge(unsigned char *p, unsigned int len) {
    if (p == NULL) return;
    // 设置 5 字节长度标识
    p[0] = ZIP_BIGLEN;
    // 写入 len
    memcpy(p+1,&len,sizeof(len));
    memrev32ifbe(p+1);
}

 //计算编码新的前置节点长度 len 所需的字节数,减去编码 p 原来的前置节点长度所需的字节数之差。
static int zipPrevLenByteDiff(unsigned char *p, unsigned int len) {
    unsigned int prevlensize;
    // 取出编码原来的前置节点长度所需的字节数
    ZIP_DECODE_PREVLENSIZE(p, prevlensize);
    // 计算编码 len 所需的字节数,然后进行减法运算
    return zipPrevEncodeLength(NULL, len) - prevlensize;
}


 // 返回指针 p 所指向的节点占用的字节数总和。
static unsigned int zipRawEntryLength(unsigned char *p) {
    unsigned int prevlensize, encoding, lensize, len;
    // 取出编码前置节点的长度所需的字节数
    ZIP_DECODE_PREVLENSIZE(p, prevlensize);
    // 取出当前节点值的编码类型,编码节点值长度所需的字节数,以及节点值的长度
    ZIP_DECODE_LENGTH(p + prevlensize, encoding, lensize, len);
    // 计算节点占用的字节数总和
    return prevlensize + lensize + len;
}

    因为压缩列表原本的数据类型是字符类型,而压缩列表所存贮的两种类型之一的字符数组则可以不用进行类型转换,所以大部分的转换是发生在整数类型的时候:

//判断encoding编码字节的长度
static unsigned int zipIntSize(unsigned char encoding) {
    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;
    default: return 0; /* 4 bit immediate */
    }
    assert(NULL);
    return 0;
}

 //看是否可以进行字符串进行整数的转换
static int zipTryEncoding(unsigned char *entry, unsigned int entrylen, long long *v, unsigned char *encoding) {
    long long value;
    // 忽略太长或太短的字符串,太长超过了整数类型的上限
    if (entrylen >= 32 || entrylen == 0) return 0;
    // 尝试转换,string2ll为util.h的工作函数,作用为完成字符串向整数的转换
    if (string2ll((char*)entry,entrylen,&value)) {
        // 转换成功,以从小到大的顺序检查适合值 value 的编码方式
        if (value >= 0 && value <= 12) {
            *encoding = ZIP_INT_IMM_MIN+value;//value直接与encoding共享同一个字节
        } 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;
        }
        // 记录值到指针
        *v = value;
        // 返回转换成功标识
        return 1;
    }
    // 转换失败
    return 0;
}


//以 encoding 指定的编码方式,将整数值 value 写入到 p 。
static void 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) {
       //无操作,因为值存贮在了encoding中
    } else {
        assert(NULL);
    }
}


//以 encoding 指定的编码方式,读取并返回指针 p 中的整数值。
static 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;
    } else {
        assert(NULL);
    }
    return ret;
}

    值得注意的是,存贮4位整数的时候,它是将值存贮在encoding中的,其一字节encoding的后四位的值便是其大小。而3字节的大小的整数值在取值时需要进行移位的操作,转化为4字节的整数,来完成取值操作。

节点的新建、插入

/*
*zl为压缩列表,p为指向的节点,s为插入的数据,slen为其长度
*/
 //根据指针 p 所指定的位置,将长度为 slen 的字符串 s 插入到 zl 中。
 //函数的返回值为完成插入操作之后的 ziplist
static unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {
    // 记录当前 ziplist 的长度
    size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), reqlen, prevlen = 0;
    size_t offset;
    int nextdiff = 0;
    unsigned char encoding = 0;
    long long value = 123456789; 
    zlentry entry, tail;
    if (p[0] != ZIP_END) {
        // 如果 p[0] 不指向列表末端,说明列表非空,并且 p 正指向列表的其中一个节点
        // 那么取出 p 所指向节点的信息,并将它保存到 entry 结构中
        // 然后用 prevlen 变量记录前置节点的长度
        // (当插入新节点之后 p 所指向的节点就成了新节点的前置节点)
        // T = O(1)
        entry = zipEntry(p);
        prevlen = entry.prevrawlen;
    } else {
        // 如果 p 指向表尾末端,那么程序需要检查列表是否为:
        // 1)如果 ptail 也指向 ZIP_END ,那么列表为空;
        // 2)如果列表不为空,那么 ptail 将指向列表的最后一个节点。
        unsigned char *ptail = ZIPLIST_ENTRY_TAIL(zl);
        if (ptail[0] != ZIP_END) {
            // 表尾节点为新节点的前置节点
            // 取出表尾节点的长度
            // T = O(1)
            prevlen = zipRawEntryLength(ptail);
        }
    }
    // 尝试看能否将输入字符串转换为整数,如果成功的话:
    // 1)value 将保存转换后的整数值
    // 2)encoding 则保存适用于 value 的编码方式
    // 无论使用什么编码, reqlen 都保存节点值的长度
    if (zipTryEncoding(s,slen,&value,&encoding)) {
        reqlen = zipIntSize(encoding);
    } else {
        reqlen = slen;
    }
    // 计算编码前置节点的长度所需的大小
    // T = O(1)
    reqlen += zipPrevEncodeLength(NULL,prevlen);
    // 计算编码当前节点值所需的大小
    reqlen += zipEncodeLength(NULL,encoding,slen);
    // 只要新节点不是被添加到列表末端,
    // 那么程序就需要检查看 p 所指向的节点(的 header)能否编码新节点的长度。
    // nextdiff 保存了新旧编码之间的字节大小差,如果这个值大于 0 
    // 那么说明需要对 p 所指向的节点(的 header )进行扩展
    nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0;
    // 因为重分配空间可能会改变 zl 的地址
    // 所以在分配之前,需要记录 zl 到 p 的偏移量,然后在分配之后依靠偏移量还原 p 
    offset = p-zl;
    // curlen 是 ziplist 原来的长度
    // reqlen 是整个新节点的长度
    // nextdiff 是新节点的后继节点扩展 header 的长度(要么 0 字节,要么 4 个字节)
    // T = O(N)
    zl = ziplistResize(zl,curlen+reqlen+nextdiff);
    p = zl+offset;
    if (p[0] != ZIP_END) {
        // 新元素之后还有节点,因为新元素的加入,需要对这些原有节点进行调整
        // 移动现有元素,为新元素的插入空间腾出位置
        //原理与顺序表插入节点一致
        memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff);
        // 将新节点的长度编码至后置节点
        // p+reqlen 定位到后置节点
        // reqlen 是新节点的长度
        zipPrevEncodeLength(p+reqlen,reqlen);
        // 更新到达表尾的偏移量,将新节点的长度也算上
        ZIPLIST_TAIL_OFFSET(zl) =intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+reqlen);
        // 如果新节点的后面有多于一个节点
        // 那么程序需要将 nextdiff 记录的字节数也计算到表尾偏移量中
        // 这样才能让表尾偏移量正确对齐表尾节点
        // T = O(1)
        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 {
        // 新元素是新的表尾节点
        ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(p-zl);
    }

    // 当 nextdiff != 0 时,新节点的后继节点的(header 部分)长度已经被改变,
    // 所以需要级联地更新后续的节点
    if (nextdiff != 0) {
        offset = p-zl;
        // T  = O(N^2)
        zl = __ziplistCascadeUpdate(zl,p+reqlen);
        p = zl+offset;
    }
    // 一切搞定,将前置节点的长度写入新节点的 header
    p += zipPrevEncodeLength(p,prevlen);
    // 将节点值的长度写入新节点的 header
    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;
}

    节点的插入有上述的讲解理解并不难,而其中出现的级联更新放在后面讲解,还有一个注意的是,更新节点数量:

# define UINT16_MAX		(65535)
#define ZIPLIST_INCR_LENGTH(zl,incr) { \
    if (ZIPLIST_LENGTH(zl) < UINT16_MAX) \
        ZIPLIST_LENGTH(zl) = intrev16ifbe(intrev16ifbe(ZIPLIST_LENGTH(zl))+incr); \
}

unsigned int ziplistLen(unsigned char *zl) {
    unsigned int len = 0;
    // 节点数小于 UINT16_MAX
    if (intrev16ifbe(ZIPLIST_LENGTH(zl)) < UINT16_MAX) {
        len = intrev16ifbe(ZIPLIST_LENGTH(zl));
    // 节点数大于 UINT16_MAX 时,需要遍历整个列表才能计算出节点数
    } else {
        unsigned char *p = zl+ZIPLIST_HEADER_SIZE;
        while (*p != ZIP_END) {
            p += zipRawEntryLength(p);
            len++;
        }
        if (len < UINT16_MAX) ZIPLIST_LENGTH(zl) = intrev16ifbe(len);
    }
    return len;
}

    在代码中可以看出,ZIPLIST_LENGTH最大值为UNIT16_MAX,也就是说最大可以保存UNIT16_MAX,而超过这个值时,需要遍历整个压缩列表才能得出压缩列表中节点的多少。

节点的删除

    节点的删除与插入原理一致,都需要对内存进行重新分配:

static 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;
    // 计算被删除节点总共占用的内存字节数
    // 以及被删除节点的总个数
    first = zipEntry(p);
    for (i = 0; p[0] != ZIP_END && i < num; i++) {
        p += zipRawEntryLength(p);
        deleted++;
    }
    // totlen 是所有被删除节点总共占用的内存字节数
    totlen = p-first.p;
    if (totlen > 0) {
        if (p[0] != ZIP_END) {
            // 执行这里,表示被删除节点之后仍然有节点存在
            // 因为位于被删除范围之后的第一个节点的 header 部分的大小
            // 可能容纳不了新的前置节点,所以需要计算新旧前置节点之间的字节数差
            nextdiff = zipPrevLenByteDiff(p,first.prevrawlen);
            // 如果有需要的话,将指针 p 后退 nextdiff 字节,为新 header 空出空间
            p -= nextdiff;
            // 将 first 的前置节点的长度编码至 p 中
            zipPrevEncodeLength(p,first.prevrawlen);
            // 更新到达表尾的偏移量
            ZIPLIST_TAIL_OFFSET(zl) =
                intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))-totlen);
            // 如果被删除节点之后,有多于一个节点
            // 那么程序需要将 nextdiff 记录的字节数也计算到表尾偏移量中
            // 这样才能让表尾偏移量正确对齐表尾节点
            tail = zipEntry(p);
            if (p[tail.headersize+tail.len] != ZIP_END) {
                ZIPLIST_TAIL_OFFSET(zl) =
                   intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+nextdiff);
            }
            // 从表尾向表头移动数据,覆盖被删除节点的数据
            memmove(first.p,p,
                intrev32ifbe(ZIPLIST_BYTES(zl))-(p-zl)-1);
        } else {
            // 执行这里,表示被删除节点之后已经没有其他节点了
            ZIPLIST_TAIL_OFFSET(zl) =
                intrev32ifbe((first.p-zl)-first.prevrawlen);
        }
        // 缩小并更新 ziplist 的长度
        offset = first.p-zl;
        zl = ziplistResize(zl, intrev32ifbe(ZIPLIST_BYTES(zl))-totlen+nextdiff);
        ZIPLIST_INCR_LENGTH(zl,-deleted);
        p = zl+offset;
        // 如果 p 所指向的节点的大小已经变更,那么进行级联更新
        // 检查 p 之后的所有节点是否符合 ziplist 的编码要求
        if (nextdiff != 0)
            zl = __ziplistCascadeUpdate(zl,p);
    }
    return zl;
}

连锁更新

    采用pre_length来表示前一个节点的空间长度会产生一个问题 ,就是在插入删除时,插入的节点空间长度如果大于等于254个字节,也就是大于1个字节所能保存的最大数,而新节点后面的节点的pre_length是一个字节,那么就需要对其pre_length内存重新分配,让其有5个字节来保存数据。然而倘若新节点后面节点空间长度刚好处于250字节到253字节之间,对其pre_length属性重新内存分配后,那么这个节点的总长度就超过了1字节的所能存贮的最大长度,而万一这个节点后面的节点pre_length也是一个字节,那么也需要进行更新,这样就引发了连锁的更新,删除也是如此。代码如下:

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;
   //从p指向的节点遍历到尾节点
    while (p[0] != ZIP_END) {
        // 将 p 所指向的节点的信息保存到 cur 结构中
        cur = zipEntry(p);
        // 当前节点的长度
        rawlen = cur.headersize + cur.len;
        // 计算编码当前节点的长度所需的字节数
        rawlensize = zipPrevEncodeLength(NULL,rawlen);
        // 如果已经没有后续空间需要更新了,跳出
        if (p[rawlen] == ZIP_END) break;
        // 取出后续节点的信息,保存到 next 结构中
        next = zipEntry(p+rawlen);
        // 后续节点编码当前节点的空间已经足够,无须再进行任何处理,跳出
        // 可以证明,只要遇到一个空间足够的节点,
        // 那么这个节点之后的所有节点的空间都是足够的
        if (next.prevrawlen == rawlen) break;
        if (next.prevrawlensize < rawlensize) {
            // 执行到这里,表示 next 空间的大小不足以编码 cur 的长度
            // 所以程序需要对 next 节点的(header 部分)空间进行扩展
            // 记录 p 的偏移量
            offset = p-zl;
            // 计算需要增加的节点数量
            extra = rawlensize-next.prevrawlensize;
            // 扩展 zl 的大小
            zl = ziplistResize(zl,curlen+extra);
            // 还原指针 p
            p = zl+offset;
            np = p+rawlen;
            noffset = np-zl;
            // 当 next 节点不是表尾节点时,更新列表到表尾节点的偏移量
            if ((zl+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))) != np) {
                ZIPLIST_TAIL_OFFSET(zl) =intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+extra);
            }
            // 向后移动 cur 节点之后的数据,为 cur 的新 header 腾出空间
            memmove(np+rawlensize,
                np+next.prevrawlensize,
                curlen-noffset-next.prevrawlensize-1);
            // 将新的前一节点长度值编码进新的 next 节点的 header
            zipPrevEncodeLength(np,rawlen);
            // 移动指针,继续处理下个节点
            p += rawlen;
            curlen += extra;
        } else {
            if (next.prevrawlensize > rawlensize) {
                // 执行到这里,说明 next 节点编码前置节点的 header 空间有 5 字节
                // 而编码 rawlen 只需要 1 字节
                // 但是程序不会对 next 进行缩小,
                // 所以这里只将 rawlen 写入 5 字节的 header 中就算了。
                zipPrevEncodeLengthForceLarge(p+rawlen,rawlen);
            } else {
                // 运行到这里,
                // 说明 cur 节点的长度正好可以编码到 next 节点的 header 中
                zipPrevEncodeLength(p+rawlen,rawlen);
            }
            break;
        }
    }
    return zl;
}

    连锁更新的原理很简单,就是从引起更新节点的位置开始往后遍历,碰到一个需要更新的节点就更新,直到第一个不需要更新的节点出现或者到达压缩列表的尾部,遍历结束。

总结

1. 压缩列表是为节约内存而开发的,而其本质上是一个字符串
2. 压缩列表所有的结构部分都由动态申请内存生成,包括其中的节点
3. 压缩列表中增加或删除节点可能会引发连锁更新
4. 压缩列表是列表键与哈希键的底层实现之一

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值