Redis源码学习记录:列表 (ziplist)

news2024/11/19 21:28:38

ziplist

redis 源码版本:6.0.9。ziplist 的代码均在 ziplist.c / ziplist.h 文件中。

定义

ziplist总体布局如下:

<zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>

  • zlbytesuint32_t,记录整个 ziplist 占用的字节数,包括 zlbytes 占用的4字节。
  • zltailuint32_t,记录从 ziplist 起始位置到最后一个节点的偏移量, 用于支持链表从尾部弹出或反向(从尾到头)遍历链表。
  • zllenuint16_t,记录节点数量, 如果存在超过 2 16 − 2 2^{16}-2 2162 个节点, 则这个值设置为 2 16 − 1 2^{16}-1 2161,这时需要遍历整个 ziplist 获取真正的节点数量。
  • zlenduint8_t,一个特殊的标志节点, 等于 255,标志 ziplist 结尾。其他节点数据不会以 255 开头。

entry 就是 ziplist 中保存的节点。entry 的格式如下:

<prevlen> <encoding> <entry-data>

  • entry-data:该节点元素,即节点存储的数据。
  • prevlen:记录前驱节点长度,单位为字节, 该属性长度为1字节或5字节。
    • 如果前驱节点长度小于254,则使用1字节存储前驱节点长度。
    • 否则,使用5字节,并且第 1 个字节固定为254,剩下4个字节存储前驱节点长度。
  • encoding:代表当前节点元素的编码格式, 包含编码类型和节点长度。 一个ziplist中,不同节点元素的编码格式可以不同。编码格式规范如下:
    1. 00pppppp( pppppp 代表 encoding 的低 6 位,下同):字符串编码,长度小于或等于 63 ( 2 6 − 1 2^6-1 261),长度存放在 encoding 的低 6 位中。
    2. 01pppppp:字符串编码, 长度小于或等于16383(24-1),长度存放在 encoding 的后 6 位和 encoding 后 1 字节中。
    3. 10b00000:字符串编码,长度大于 16383 ( 2 14 − 1 2^{14}-1 2141),长度存放在 encoding 后 4 字节中。
    4. 11000000:数值编码, 类型为 int16_t,占用 2 字节。
    5. 11010000:数值编码,类型为 int32_t, 占用 4 字节。
    6. 11100000:数值编码,类型为 int64_t,占用 8 字节。
    7. 11110000:数值编码,使用 3 字节保存一个整数。
    8. 11111110:数值编码,使用 1 字节保存一个整数。
    9. 1111xxxx:使用 encoding 低 4 位存储一个整数, 存储数值范围为 0 ∼ 12 0\sim12 012。该编码下 encoding 低 4 位的可用范围为 0001 ∼ 1101 0001\sim1101 00011101encoding 低 4 位减 1 为实际存储的值。
    10. 11111111:255,ziplist 结束节点。

注意第 ②、③ 种编码格式,除了 encoding 属性, 还需要额外的空间存储节点元素长度。第 ⑨ 种格式也比较特殊,节点元素直接存放在 encoding属性上。 该编码是针对小数字的优化。这时 entry-data 为空。

字节序

encoding 属性使用多个字节存储节点元素长度, 这种多字节数据存储在计算机内存中或者进行网络传输时的字节顺序称为字节序,字节序有两种类型: 大端字节序和小端字节序。

  • 大端字节序: 低字节数据保存在内存高地址位置, 高字节数据保存在内存低地址位置。
  • 小端字节序: 低字节数据保存在内存低地址位置, 高字节数据保存在内存高地址位置。

数值 0x44332211 的大端字节序和小端字节序存储方式如下图所示。

img

CPU 处理指令通常是按照内存地址增长方向执行的。 使用小端字节序, CPU 可以先读取并处理低位字节,执行计算的借位、 进位操作时效率更高。 大端字节序则更符合人们的读写习惯。

ziplist采取的是小端字节序。

下面是 Redis 提供的一个简单例子:

img

  • [0f 00 00 00]zlbytes 为 15,代表整个 ziplist 占用 15 字节,注意该数值以小端字节序存储。
  • [0c 00 00 00]zltail 为 12,代表从 ziplist 起始位置到最后 一个节点 ([02 f6]) 的偏移量。
  • [02 00]zllen为 2,代表 ziplist 中有 2 个节点。
  • [00 f3]:00 代表前一个节点长度,f3 使用了 encoding 第 ⑨ 种编码格式,存储数据为 encoding 低 4 位减 1,即 2。
  • [02 f6]:02 代表前一个节点长度为 2 字节, f5 编码格式同上,存储数据为 5。
  • [ff]:结束标志节点。

ziplist 是 Redis 中比较复杂的数据结构,可以先结合上述属性说明和例子,理解 ziplist 中数据的存放格式。等会儿看了部分源代码之后就比较好理解啦!

ziplistFind

  • 参数:
    • p:指定从 ziplist 的那个节点开始查找。
    • vstr:待查找元素的内容。
    • vlen:待查找元素的长度。
    • skip:间隔多少个节点才执行一次元素对比操作。
  • 返回值:如果找到了目的元素,返回该节点的首地址,如果没有找到目的元素,返回 NULL
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;

        ZIP_DECODE_PREVLENSIZE(p, prevlensize); // 获取 prevlen 字段的字节数,并将结果保存到 prevlensize 变量中
        ZIP_DECODE_LENGTH(p + prevlensize, encoding, lensize, len); // 获取 encoding 字段的字节数,并将结果保存到 lensize 字段中;获取 ziplist 存储数据的字节数,并将结果保存到 len 中。
        q = p + prevlensize + lensize; // 指向 ziplist 存储的数据啦

        if (skipcnt == 0) {
            if (ZIP_IS_STR(encoding)) { // 如果 encoding 是字符串编码
                if (len == vlen && memcmp(q, vstr, vlen) == 0) { // 如果找到了目的元素
                    return p; // 返回节点的首地址
                }
            } else { // 如果 encoding 不是字符串编码
                // 确保对查找元素只进行一次数值编码
                if (vencoding == 0) {
                    // 对查找的元素进行数值编码,如果编码成功将数值保存到 vll,将编码方式保存到 vencoding 修改了 vencoding 确保在查找目的元素的时候只会进行一次数值编码
                    if (!zipTryEncoding(vstr, vlen, &vll, &vencoding)) {
                        // 进行数值编码失败了!
                        vencoding = UCHAR_MAX;
                    }
                    // 其实无论是数值编码成功还是失败 vencoding 理论上都不会为 0,但是为了防止程序出问题还是加上了这个断言。再说 release 版本下 assert 根本就没用嘛!
                    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; // 相等的话,证明找到了,返回节点的首地址
                    }
                }
            }
			
            // 重置 skipcnt 
            skipcnt = skip;
        } else {
            // skipcnt 不为 0 该节点需要跳过哦!
            skipcnt--;
        }

        p = q + len; // 下一个 ziplist 节点的首地址
    }

    return NULL;
}

ZIP_DECODE_PREVLENSIZE

宏功能:获取 prevlen 字段的字节数,并将结果保存到 prevlensize 变量中。

  • ZIP_BIG_PREVLEN#define ZIP_BIG_PREVLEN 254
  • prevlen 字段存储的是前驱节点的长度,单位是字节,该属性长度为 1 字节或 5 字节。
    • 如果前驱节点长度小于 254,则使用 1 字节存储前驱节点长度。
    • 否则使用 5 字节,并且第一个字节固定为 254,剩下 4 字节存储前驱节点的长度。
#define ZIP_DECODE_PREVLENSIZE(ptr, prevlensize) do {                          \
    if ((ptr)[0] < ZIP_BIG_PREVLEN) { //前驱节点长度小于 254                     \
        (prevlensize) = 1; // prevlen 则是 1 字节                               \
    } else {                                                                   \
        (prevlensize) = 5; // 否则 prevlen 则是 5 字节                           \
    }                                                                          \
} while(0)

ZIP_DECODE_LENGTH

宏功能:获取 encoding 字段的字节数,并将结果保存到 lensize 字段中;获取 ziplist 存储数据的字节数,并将结果保存到 len 中。

  • ptr:指向 encoding,即跳过了 prevlen 字段指向 encoding 字段。

  • ZIP_STR_06B#define ZIP_STR_06B (0 << 6)

  • ZIP_STR_14B#define ZIP_STR_14B (1 << 6)

  • ZIP_STR_32B#define ZIP_STR_32B (2 << 6)

#define ZIP_DECODE_LENGTH(ptr, encoding, lensize, len) do {                                 \
    ZIP_ENTRY_ENCODING((ptr), (encoding)); // 获取编码方式,将结果保存到 encoding 中            \
    if ((encoding) < ZIP_STR_MASK) { // 如果是字符串编码                                      \
        if ((encoding) == ZIP_STR_06B) { // 如果是字符串的 00pp pppp 编码方式                  \
            (lensize) = 1; // encoding 字段占 1 个字节,即 encoding 这个字节                   \
            (len) = (ptr)[0] & 0x3f; // (00pp pppp) & (0011 1111) 获取字符串的长度            \
        } else if ((encoding) == ZIP_STR_14B) { // 如果是字符串的 01pp pppp 编码方式           \
            (lensize) = 2; // encoding 字段占 2 个字节:encoding 这个字节+encoding 后 1 字节    \
            (len) = (((ptr)[0] & 0x3f) << 8) | (ptr)[1]; // 获取字符串的长度 [1](见注解1)      \
        } else if ((encoding) == ZIP_STR_32B) {  // 如果是字符串的 1000 0000 编码方式          \
            (lensize) = 5; // encoding 字段占 5 个字节:encoding 这个字节+encoding 后 4 字节    \
            (len) = ((ptr)[1] << 24) |                                                      \
                    ((ptr)[2] << 16) |                                                      \
                    ((ptr)[3] <<  8) |                                                      \
                    ((ptr)[4]); // 获取字符串长度,原理同上一个 if 分支,这里就不再讲了            \
        } else {  // 无效的字符串编码格式                                                      \
            panic("Invalid string encoding 0x%02X", (encoding));                            \
        }                                                                                   \
    } else { // 如果是数值编码                                                                \
        (lensize) = 1; // encoding 字段占 1 个字节                                            \
        (len) = zipIntSize(encoding); // 根据 encoding 获取数值编码下存储数值需要的字节数        \
    }                                                                                       \
} while(0)
  1. (len) = (((ptr)[0] & 0x3f) << 8) | (ptr)[1];
    • ptr[0]encoding 字段的第一个字节。
    • (ptr)[0] & 0x3f:获取 01pp pppp 字符串编码下 encoding 第一个字节的低 6 位。
    • (((ptr)[0] & 0x3f) << 8) | (ptr)[1]:获取到 encoding 第一字节的低 6 位之后,与 encoding 的第二字节拼接,最终获取到字符串的长度。

ZIP_ENTRY_ENCODING

宏功能:获取编码方式,将结果保存到 encoding 变量中。

  • ZIP_STR_MASK #define ZIP_STR_MASK 0xc0

  • encoding 是字符串编码时,encoding 的第一个字节如果小于 0xc0 那么可以确定是字符串编码。

    • 00pppppp ( pppppp 代表 encoding 的低6位,下同):字符串编码,长度小于或等于 63 ( 2 6 2^6 26 - 1),长度存放在 encoding 的低 6 位中。
    • 01pppppp:字符串编码,长度小于或等于 16383 ( 2 14 2^{14} 214 - 1),长度存放在 encoding 的后 6 位和 encoding 后 1 字节中。
    • 10000000:字符串编码,长度大于 16383 ( 2 14 2^{14} 214- 1),长度存放在 encoding 后 4 字节中。
    • 11000000:数值编码,类型为 int16_t,占用 2 字节。
    • 11010000:数值编码,类型为 int32_t,占用 4 字节。
    • 11100000:数值编码,类型为 int64_t,占用 8 字节。
    • 11110000:数值编码,使用 3 字节保存一个整数。
    • 11111110:数值编码,使用 1 字节保存 一个整数。
    • 1111xxxx:使用 encoding 低4位存储一个整数, 存储数值范围为 0 ∼ 12 0\sim12 012。该编码下 encoding 低 4 位的可用范围为 0001 ∼ 1101 0001\sim1101 00011101encoding 低 4 位减 1 为实际存储的值。
    • 11111111:255,ziplist 结束节点。

    可以看到即使 encoding 编码为字符串时 p 全部取 1,encoding 的首字节还是小于 0xc0,因此可以通过 encoding 首字节的数值大小与 0xc0 比较来确定编码时字符串还是数值。

#define ZIP_ENTRY_ENCODING(ptr, encoding) do {  \
    (encoding) = (ptr[0]); // ptr 指向的就是 encoding           \
    if ((encoding) < ZIP_STR_MASK) (encoding) &= ZIP_STR_MASK; // 获取 encoding 的高 2 位进一步确定编码方式! \
} while(0)

为什么不直接让 encoding &= 0xc0 呢?因为后面需要用 encoding 变量来区分具体是哪一种数值编码。

zipIntSize

函数功能:根据参数 encoding,获取并返回该编码方式下,存储数值需要的字节数。

  • ZIP_INT_8B#define ZIP_INT_8B 0xfe1111 1110 数值编码方式。
  • ZIP_INT_16B#define ZIP_INT_16B (0xc0 | 0<<4)1100 0000 数值编码方式。
  • ZIP_INT_24B#define ZIP_INT_24B (0xc0 | 3<<4)1111 0000 数值编码方式
  • ZIP_INT_32B#define ZIP_INT_32B (0xc0 | 1<<4)1101 0000 数值编码方式
  • ZIP_INT_64B#define ZIP_INT_64B (0xc0 | 2<<4)1110 0000 数值编码方式
  • ZIP_INT_IMM_MIN#define ZIP_INT_IMM_MIN 0xf11111 xxxx 数值编码方式编码数字的最小值。
  • ZIP_INT_IMM_MAX#define ZIP_INT_IMM_MAX 0xfd1111 xxxx 数值编码方式编码数字的最大值。
unsigned int zipIntSize(unsigned char encoding) {
    switch(encoding) {
    case ZIP_INT_8B:  return 1; // 1111 1110 数值编码方式,使用 1 个字节保存整数
    case ZIP_INT_16B: return 2; // 1100 0000 数值编码方式,使用 2 个字节保存整数
    case ZIP_INT_24B: return 3; // 1111 0000 数值编码方式,使用 3 个字节保存整数
    case ZIP_INT_32B: return 4; // 1101 0000 数值编码方式,使用 4 个字节保存整数
    case ZIP_INT_64B: return 8; // 1110 0000 数值编码方式,使用 8 个字节保存整数
    }
    
    // 1111 xxxx 数值编码方式,使用 encoding 存储整数,不需要多余的空间,xxxx 的取值范围是: 0001~1101
    // 对应了 ZIP_INT_IMM_MIN 和 ZIP_INT_IMM_MAX,实际存储的整数是 [0000~1100] = [0, 12]
    if (encoding >= ZIP_INT_IMM_MIN && encoding <= ZIP_INT_IMM_MAX)
        return 0; // 直接将整数存储到 encoding 中是对小数字的优化!
    panic("Invalid integer encoding 0x%02X", encoding); // 无效的整形编码
    return 0;
}

ZIP_IS_STR

宏功能:判断 enc 的编码方式是否是字符串编码!

  • ZIP_STR_MASK#define ZIP_STR_MASK 0xc0

  • 字符串编码的 encoding 最高的两个比特位是:000111 中的一个,肯定小于 0xc0 最高的两个比特位 11 嘛!并且数值编码的最高两个比特位都是 11

#define ZIP_IS_STR(enc) (((enc) & ZIP_STR_MASK) < ZIP_STR_MASK)

zipTryEncoding

函数功能:传入一个字符串,尝试对其进行数值编码。如果成功,返回 1,并将编码成功的数值保存到 *v,将数值对应的编码方式保存到 *encoding;如果失败,返回 0。

  • ZIP_INT_8B#define ZIP_INT_8B 0xfe1111 1110 数值编码方式。
  • ZIP_INT_16B#define ZIP_INT_16B (0xc0 | 0<<4)1100 0000 数值编码方式。
  • ZIP_INT_24B#define ZIP_INT_24B (0xc0 | 3<<4)1111 0000 数值编码方式。
  • ZIP_INT_32B#define ZIP_INT_32B (0xc0 | 1<<4)1101 0000 数值编码方式。
  • ZIP_INT_64B#define ZIP_INT_64B (0xc0 | 2<<4)1110 0000 数值编码方式。
  • ZIP_INT_IMM_MIN#define ZIP_INT_IMM_MIN 0xf11111 xxxx 数值编码方式编码数字的最小值。
  • ZIP_INT_IMM_MAX#define ZIP_INT_IMM_MAX 0xfd1111 xxxx 数值编码方式编码数字的最大值。
int zipTryEncoding(unsigned char *entry, unsigned int entrylen, long long *v, unsigned char *encoding) {
    long long value;

    if (entrylen >= 32 || entrylen == 0) return 0; // 满足这个条件编码一定会失败的 long long 不可能对 32 个字符的数字进行编码,entyrlen 为 0 一定编码失败
    if (string2ll((char*)entry,entrylen,&value)) { // 这个函数讲过啦在字符串的那一节,字符串转 long long 
        // 如果转化成 long long 成功,根据转换的结果进行判断
        if (value >= 0 && value <= 12) {
            *encoding = ZIP_INT_IMM_MIN+value; // 1111 xxxx 数值编码方式
        } else if (value >= INT8_MIN && value <= INT8_MAX) {
            *encoding = ZIP_INT_8B; // 1111 1110 数值编码方式。
        } else if (value >= INT16_MIN && value <= INT16_MAX) {
            *encoding = ZIP_INT_16B; // 1100 0000 数值编码方式
        } else if (value >= INT24_MIN && value <= INT24_MAX) {
            *encoding = ZIP_INT_24B; // 1111 0000 数值编码方式
        } else if (value >= INT32_MIN && value <= INT32_MAX) {
            *encoding = ZIP_INT_32B; // 1101 0000 数值编码方式
        } else {
            *encoding = ZIP_INT_64B; // 1110 0000 数值编码方式
        }
        *v = value; // 将结果保存到 *v 
        return 1; // 转换成功
    }
    return 0; // 如果字符串无法转换为 long long 那么编码成数值就失败啦
}

zipLoadInteger

函数功能:根据 encoding 编码方式,返回 ziplist 节点存储的数值。

  • 参数:
    • p:指向 ziplist 节点存储数据空间的首字节地址。
    • encoding:数值的编码方式。
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) { // 1111 1110 数值编码方式,使用 1 个字节保存整数
        ret = ((int8_t*)p)[0]; // 获取存储的数据
    } else if (encoding == ZIP_INT_16B) { // 1100 0000 数值编码方式,使用 2 个字节保存整数
        memcpy(&i16,p,sizeof(i16)); // 临时保存转换结果
        memrev16ifbe(&i16); // 字节序转换
        ret = i16; // 保存字节序转换后的结果
    } else if (encoding == ZIP_INT_32B) { // 1101 0000 数值编码方式,使用 4 个字节保存整数
        memcpy(&i32,p,sizeof(i32));
        memrev32ifbe(&i32);
        ret = i32;
    } else if (encoding == ZIP_INT_24B) { // 1111 0000 数值编码方式,使用 3 个字节保存整数
        i32 = 0;
        memcpy(((uint8_t*)&i32)+1,p,sizeof(i32)-sizeof(uint8_t));
        memrev32ifbe(&i32);
        ret = i32>>8; // 转换字节序之后需要 >> 8 位
    } else if (encoding == ZIP_INT_64B) { // 1110 0000 数值编码方式,使用 8 个字节保存整数
        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; // 1111 xxxx 数值编码方式,规定的 xxxx - 1表示存储的数值
    } else {
        assert(NULL);
    }
    return ret; // 返回结果
}
  • ZIP_INT_IMM_MASK#define ZIP_INT_IMM_MASK 0x0f

memrev16ifbe

功能:进行字节序的转换,条件编译实现的哈!

#if (BYTE_ORDER == LITTLE_ENDIAN)
#define memrev16ifbe(p) ((void)(0))
#define memrev32ifbe(p) ((void)(0))
#define memrev64ifbe(p) ((void)(0))
#define intrev16ifbe(v) (v)
#define intrev32ifbe(v) (v)
#define intrev64ifbe(v) (v)
#else
#define memrev16ifbe(p) memrev16(p)
#define memrev32ifbe(p) memrev32(p)
#define memrev64ifbe(p) memrev64(p)
#define intrev16ifbe(v) intrev16(v)
#define intrev32ifbe(v) intrev32(v)
#define intrev64ifbe(v) intrev64(v)
#endif
#define memrev16ifbe(p) memrev16(p)

// redis 源码直接一个字节一个字节交换来实现的哈!whatever how much the size is
void memrev16(void *p) {
    unsigned char *x = p, t;

    t = x[0];
    x[0] = x[1];
    x[1] = t;
}

// ·······

void memrev64(void *p) {
    unsigned char *x = p, t;

    t = x[0];
    x[0] = x[7];
    x[7] = t;
    t = x[1];
    x[1] = x[6];
    x[6] = t;
    t = x[2];
    x[2] = x[5];
    x[5] = t;
    t = x[3];
    x[3] = x[4];
    x[4] = t;
}

ziplistInsert

  • 参数:
    • zl:待插入的 ziplist
    • p: 指向插入位置的后驱节点。
    • s:待插入元素的内容。
    • slen:待插入元素的长度。
unsigned char *ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {
    return __ziplistInsert(zl,p,s,slen);
}

__ziplistInsert

  • 参数:

    • zl:待插入的 ziplist

    • p: 指向插入位置的后驱节点。

    • s:待插入元素的内容。

    • slen:待插入元素的长度。

  • ZIP_END#define ZIP_END 255ziplist 的结束节点的 encoding 是 255 哈,ziplist 结束节点仅含 encoding 字段。

unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {
    size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), reqlen; // curlen 整个 ziplist 占用的字节数,ziplist 最开始的四字节存放的就是整个 ziplist 占用的字节数,包括这 4 字节哦!
    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(p, prevlensize, prevlen); // 获取 p 节点 prevlen 字段的字节数,并将结果保存到 prevlensize 变量中,获取前驱节点占用的字节数,并将结果保存到 prevlen 变量中。
    } else { // 是尾插
        unsigned char *ptail = ZIPLIST_ENTRY_TAIL(zl); // 获取 ziplist 尾节点,赋值给 ptail 变量
        if (ptail[0] != ZIP_END) { // 理论上来说 ptail[0] 不可能等于 ZIP_END 的。
            prevlen = zipRawEntryLength(ptail); // 获取 ptail 这个节点的字节数 赋值给 prevlen
            // 因为 ziplist 结束节点并没有保存前一个节点的所占的字节数,需要手动计算 
        }
    }

    // 尝试对要插入的字符串 s 进行数值编码!
    if (zipTryEncoding(s,slen,&value,&encoding)) {
        // 如果数值编码成功
        reqlen = zipIntSize(encoding); // 根据参数 encoding,获取并返回该编码方式下,存储数值需要的字节数。将得到的结果赋值给 reqlen
    } else {
        // 数值编码失败,reqlen 就是字符串的长度。
        reqlen = slen;
    }
    /* We need space for both the length of the previous entry and
     * the length of the payload. */
    
    reqlen += zipStorePrevEntryLength(NULL,prevlen); // 参数 1 为 NULL,返回存储前驱节点所占字节数需要的 prevlen 字段的字节数。最后加到 reqlen 上。
    reqlen += zipStoreEntryEncoding(NULL,encoding,slen); // 参数 1 为 NULL,返回 encoding 字段所占的字节数。最后加到 reqlen 上。

    /* 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;
    nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0; // 将后驱节点的 prevlen 字段需要调整的字节数保存到 nextdiff 变量中。
    if (nextdiff == -4 && reqlen < 4) { // [2](见注解2)
        nextdiff = 0; // 强制修改 nextdiff = 0,使之不要缩容
        forcelarge = 1;
    }

    /* Store offset because a realloc may change the address of zl. */
    offset = p-zl; // 记录待插入节点的后驱节点相对于 ziplist 首地址的偏移量,因为 realloc 可能会修改ziplist 的首地址。 
    zl = ziplistResize(zl,curlen+reqlen+nextdiff); // 开辟新的空间出来哈,包括插入节点的空间,nextdiff 需要调整的空间嘛!该函数会在最后一个字节直接设置 zlend。
    p = zl+offset; // 带插入元素的后驱节点的首地址

    /* Apply memory move when necessary and update tail offset. */
    if (p[0] != ZIP_END) { // 不是尾插
        memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff); // 移动数据,留出空间放新插入的节点,curlen-offset-1+nextdiff 这个 -1 说明没有移动那个 zlend 字段哈!因为在 ziplistResize 函数中已经设置过了嘛

        if (forcelarge)
            zipStorePrevEntryLengthLarge(p+reqlen,reqlen); // forcelarge 这个名字还是能够理解他的意思的!理论上新插入节点的后驱节点的 prevlen 字段一个字节就够了嘛,但是为了防止缩容情况的出现,被迫让 prevlen 字段占用了 5 字节来存储新插入节点的长度!所以叫 forcelarge,可以理解!
        else
            zipStorePrevEntryLength(p+reqlen,reqlen); // 给新插入节点的后驱节点的 prevlen 字段赋值

        // 更新 ziplist 的 zltail 字段的值
        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. */
        zipEntry(p+reqlen, &tail); // 构建 zlentry 结构体
        if (p[reqlen+tail.headersize+tail.len] != ZIP_END) { // 如果有多个后驱节点,则还需要加上 nextdiff [1](见注解1)
            ZIPLIST_TAIL_OFFSET(zl) =
                intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+nextdiff);
        }
    } else {
        // 更新 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 */
    // 如果 nextdiff 不等于 0 说明需要更新新插入节点的后面节点的 prevlen字段的长度,prevlen 字段存储的值啥的!
    if (nextdiff != 0) {
        offset = p-zl; // 记录当前节点相对于 ziplist 首地址的偏移量,因为 __ziplistCascadeUpdate 函数可能会修改 zl 指针
        zl = __ziplistCascadeUpdate(zl,p+reqlen);
        p = zl+offset; // 重新赋值 p 
    }

    // 插入节点
    p += zipStorePrevEntryLength(p,prevlen); // 修改新节点 prevlen 字段的值,并且跳过新节点的 prevlen 字段,指向 encoding 字段。
    p += zipStoreEntryEncoding(p,encoding,slen); // 修改新节点 encoding 字段的值,并且跳过新节点的 encoding 字段,指向 entry_data 字段。
    if (ZIP_IS_STR(encoding)) { // 如果是字符串编码
        memcpy(p,s,slen); // 字符串的话, 直接 memcpy 拷贝数据就可以啦!
    } else { // 如果是数值编码
        zipSaveInteger(p,value,encoding);
    }
    ZIPLIST_INCR_LENGTH(zl,1);
    return zl;
}
  1. 如果后驱节点只有一个 nextdiff 可以忽略,因为在原偏移量的基础上加上 reqlen 就是尾节点的指针。但是如果有多个后驱节点,情况就不同啦!

    img

  2. 如果 reqlen < 4 && nextdiff == -4 时,不对 nextdiff 做调整,那么等会儿调用 ziplistResize 函数,他的参数 curlen+reqlen+nextdiff < curlen,就会进行缩容,导致原 ziplist 的数据丢失,所以我们需要对 nextdiff 做调整,防止数据丢失。

    这只是一方面的原因哈!还有一个原因:出现缩容的情况将 mextdiff 强制设置为 0,就可以避免缩容情况下导致的级联更新!所以强制保持后驱节点的 prevlen 字段保持不变。

intrev32ifbe

  • 宏功能:字节序转换,并将转换后的结果返回。相比于 memrev 系列函数多了返回值!
#define intrev32ifbe(v) intrev32(v)

uint32_t intrev32(uint32_t v) {
    memrev32(&v);
    return v;
}

void memrev32(void *p) {
    unsigned char *x = p, t;

    t = x[0];
    x[0] = x[3];
    x[3] = t;
    t = x[1];
    x[1] = x[2];
    x[2] = t;
}

ZIPLIST_BYTES

宏功能:将 zl 指针,转换为 uint32_t* 并解引用。 即获取整个 ziplist 占用的字节数。

#define ZIPLIST_BYTES(zl)       (*((uint32_t*)(zl)))

ZIP_DECODE_PREVLEN

宏功能:获取 prevlen 字段的字节数,并将结果保存到 prevlensize 变量中,获取前驱节点占用的字节数,并将结果保存到 prevlen 变量中。

#define ZIP_DECODE_PREVLEN(ptr, prevlensize, prevlen) do {                               \
    ZIP_DECODE_PREVLENSIZE(ptr, prevlensize); // 获取 prevlen 字段的字节数,并将结果保存到 prevlensize 变量中                                                                        \
    if ((prevlensize) == 1) {                                                            \
        (prevlen) = (ptr)[0]; // 将前驱节点的字节数保存到 prevlen 中                         \
    } else if ((prevlensize) == 5) {                                                     \
        assert(sizeof((prevlen)) == 4); // debug 模式下的强制检查,确保 prevlen 是 4 字节变量 \
        memcpy(&(prevlen), ((char*)(ptr)) + 1, 4); // 当前驱节点的大小大于等于 254,prevlen 字段的第一个字节存储的是 254,第 2-5 四个字节存储的是前驱节点的字节数                                  \
        memrev32ifbe(&prevlen); // 字节序的转换                                             \
    }                                                                          \
} while(0)

ZIPLIST_ENTRY_TAIL

宏功能:根据 ziplist 的首地址找到 zltail 字段后,解引用获取到最后一个节点的偏移量,并对 zl 字段加上该偏移量,指向 ziplist 的尾节点。ziplist 的首地址向后偏移 4 个字节就是 zltail 字段,该字段是 uint32_t 类型,记录从 ziplit 其实位置到最后一个节点的偏移量。

#define ZIPLIST_ENTRY_TAIL(zl)  ((zl)+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl)))

#define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t))))

zipRawEntryLength

  • 函数功能:返回节点 p 占用的字节数。

  • 参数:ziplist 尾节点的指针。

  • 返回值:整个节点的字节数。

unsigned int zipRawEntryLength(unsigned char *p) {
    unsigned int prevlensize, encoding, lensize, len;
    ZIP_DECODE_PREVLENSIZE(p, prevlensize); // 获取 prevlen 字段的字节数,将结果保存到 prevlensize 变量中。
    ZIP_DECODE_LENGTH(p + prevlensize, encoding, lensize, len); // 获取 encoding 字段的字节数,并将结果保存到 lensize 字段中;获取 ziplist 存储数据的字节数,并将结果保存到 `len` 中。
    return prevlensize + lensize + len; // 返回整个节点的字节数。
}

zipStorePrevEntryLength

  • 功能:如果参数 1 为 NULL 则返回存储 len 需要的 prevlen 字段的字节数;如果参数 1 不为 NULL 除了返回 prevlen 字段所占的字节数,还会对参数 1 指向节点的 prevlen 字段进行赋值。

  • 参数:

    • p:节点首地址。
    • len:节点长度。
  • ZIP_BIG_PREVLEN#define ZIP_BIG_PREVLEN 254。如果前驱节点长度小于 254,使用 1 字节存储前驱节点长度。如果前驱节点长度大于等于 254,使用 5 字节,第一个字节固定为 254,剩下的 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) { // 如果 p 没有指向一个 ziplist 的节点
        return (len < ZIP_BIG_PREVLEN) ? 1 : sizeof(len)+1; // 返回 prevlen 字段的大小,len < 254 返回 1 否则返回 5 嘛
    } else { // 如果 p 指向了一个 ziplist 的节点
        if (len < ZIP_BIG_PREVLEN) { // 如果说节点的长度小于 254
            p[0] = len; // 给 prevlen 字段赋值
            return 1;
        } else { // 如果说节点的长度大于等于 254
            return zipStorePrevEntryLengthLarge(p,len); // 也是给 prevlen 字段赋值
        }
    }
}

zipStorePrevEntryLengthLarge

  • 功能:参数 2 的长度大于 254 时,调用的这个函数,因此该函数固定返回 5 哈。如果参数 1 为 NULL 则返回 prevlen 字段所占的字节数;如果参数 1 不为 NULL 除了返回 prevlen 字段所占的字节数,还会对参数 1 指向节点的 prevlen 字段进行赋值。
  • 参数:
    • p:节点首地址。
    • len:节点长度。
  • 返回值:prevlen 字段所占的字节数,固定为 5。
int zipStorePrevEntryLengthLarge(unsigned char *p, unsigned int len) {
    if (p != NULL) { // 如果 p 不等于 NULL
        p[0] = ZIP_BIG_PREVLEN; // 节点长度大于等于 254 prevlen 字段的第一个字节固定是 254 嘛
        memcpy(p+1,&len,sizeof(len)); // 剩下的四个字节存储的是实际的长度嘛
        memrev32ifbe(p+1); // 转化字节序
    }
    return 1+sizeof(len); // 返回 prevlen 字段的字节数 + encoding 字段的 1 字节
}

zipStoreEntryEncoding

  • 函数功能:如果参数 1 为 NULL 则返回 encoding 字段所占的字节数;如果参数 1 不为 NULL,还会对参数 1 指向节点的 encoding 字段的值进行修改!
  • 参数:
    • p:节点首地址。
    • encoding:节点编码。
    • rawlen:待插入元素的长度。
  • ZIP_STR_06B#define ZIP_STR_06B (0 << 6)
  • ZIP_STR_14B#define ZIP_STR_14B (1 << 6)
  • ZIP_STR_32B#define ZIP_STR_32B (2 << 6)
unsigned int zipStoreEntryEncoding(unsigned char *p, unsigned char encoding, unsigned int rawlen) {
    unsigned char len = 1, buf[5]; // len 初始化为 1,encoding 字段最小占一字节

    if (ZIP_IS_STR(encoding)) { // 判断是否是字符串编码,在 __ziplistInsert 函数中,encoding 字段是被初始化为 0 的嘛,如果数值编码转换失败了,encoding 还是 0 ,那么就是字符串编码啦!高 2 位 00 < 11 嘛
        if (rawlen <= 0x3f) { // 00pp pppp 编码,字符串的长度存放在 encoding 的低 6 位
            if (!p) return len;
            buf[0] = ZIP_STR_06B | rawlen;
        } else if (rawlen <= 0x3fff) { // 01pp pppp 编码。字符串的长度存放在 encoding 的低 6 位和 encoding 的后一字节
            len += 1; // encoding 字段占 2 字节
            if (!p) return len; // if p==NULL 直接就返回 encoding 字段的长度啦
            buf[0] = ZIP_STR_14B | ((rawlen >> 8) & 0x3f); // encoding 字段的第一个字节
            buf[1] = rawlen & 0xff; // encoding 字段的第二个字节
        } else { // 1000 0000 编码,长度存放在 encoding 的后四字节
            len += 4; // encoding 字段所占的字节数为 5
            if (!p) return len; // if p==NULL 直接就返回 encoding 字段的长度啦
            buf[0] = ZIP_STR_32B; // encoding 字段的第一个字节
            buf[1] = (rawlen >> 24) & 0xff; // encoding 字段的第一个字节
            buf[2] = (rawlen >> 16) & 0xff; // encoding 字段的第二个字节
            buf[3] = (rawlen >> 8) & 0xff; // encoding 字段的第三个字节
            buf[4] = rawlen & 0xff; // encoding 字段的第四个字节
        }
    } else { // 数值编码
        if (!p) return len; // if p==NULL 直接就返回 encoding 字段的长度啦
        buf[0] = encoding; // encoding 字段的第一个字节。
    }

    // 到这里 p 一定不为 NULL,将 encoding 字段做修改!
    memcpy(p,buf,len);
    return len; // 返回 encoding 字段所占的字节数
}

zipPrevLenByteDiff

  • 功能:传入待插入节点的后驱节点指针,和新插入节点的长度,判断待插入节点的 prevlen 字段能否存储新插入节点所占的字节数。
  • 参数:
    • p:指向插入位置的后驱节点。
    • len:待插入节点的长度。
  • 返回值:后驱节点的 prevlen 字段需要调整多少个字节!
    • 0:后驱节点的 prevlen 字段不需要调整大小。
    • -4:后驱节点的 prevlen 字段需要减少 4 字节。
    • 4:后驱节点的 prevlen 字段需要增加 4 字节。
int zipPrevLenByteDiff(unsigned char *p, unsigned int len) {
    unsigned int prevlensize;
    ZIP_DECODE_PREVLENSIZE(p, prevlensize); // 解析后驱节点 prevlen 字段的字节数,并将结果保存到 prevlensize 变量中
    return zipStorePrevEntryLength(NULL, len) - prevlensize; // [1](见注解1)
}
  1. zipStorePrevEntryLength(NULL, len) 参数 1 为 NULL 返回存储 len 需要的 prevlen 字段的字节数。 然后与 prevlensize 做差,就能知道待插入节点的后驱节点的 prevlen 字段的字节数能否用来存储新节点所占的字节数。

ziplistResize

  • 函数功能:传入 ziplist 首地址,重新分配这个 ziplist 的空间,空间的大小为 len
unsigned char *ziplistResize(unsigned char *zl, unsigned int len) {
    zl = zrealloc(zl,len); // realloc 空间
    ZIPLIST_BYTES(zl) = intrev32ifbe(len); // 更新 zlbytes 字段
    zl[len-1] = ZIP_END; // 在 realloc 出来的空间的最后一个字节加上 zlend 字段
    return zl;
}

ZIPLIST_TAIL_OFFSET

宏功能:返回 ziplist 起始位置到最后一个节点的偏移量。

#define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t))))

zipEntry

  • 函数功能:传入待插入节点的首地址,获取节点相关的数据,用来初始化 zlentry 这个结构体。
  • 参数:
    • p:节点首地址。
    • e:传入一个 zlentry 结构体的地址。
void zipEntry(unsigned char *p, zlentry *e) {

    ZIP_DECODE_PREVLEN(p, e->prevrawlensize, e->prevrawlen); // 获取 prevlen 字段的字节数,并将结果保存到 e->prevrawlensize 中,获取前驱节点(新插入节点)占用的字节数,并将结果保存到 e->prevrawlen 中。
    ZIP_DECODE_LENGTH(p + e->prevrawlensize, e->encoding, e->lensize, e->len); // 获取 encoding 字段的字节数,并将结果保存到 e->lensize 字段中;获取节点 p 存储数据的字节数,并将结果保存到 e->len 中。
    e->headersize = e->prevrawlensize + e->lensize; // 计算 headersize 并将结果保存到 e->headersize
    e->p = p; // e->p 指向节点的首地址
}

zlentry

/* We use this function to receive information about a ziplist entry.
 * Note that this is not how the data is actually encoded, is just what we
 * get filled by a function in order to operate more easily. */
// 翻译:我们使用这个结构体来接收有关一个 ziplist 条目的信息。请注意,这并不是数据实际编码的方式,而是我们通过一个结构体填充以便更轻松操作的内容。
typedef struct zlentry {
    // prevlen 字段占用的字节数
    unsigned int prevrawlensize; /* Bytes used to encode the previous entry len*/
    // 前驱节点的字节数
    unsigned int prevrawlen;     /* Previous entry len. */
    // encoding 字段占用的字节数
    unsigned int lensize;        /* Bytes used to encode this entry type/len.
                                    For example strings have a 1, 2 or 5 bytes
                                    header. Integers always use a single byte.*/
    // 存储数据占用的字节数
    unsigned int len;            /* Bytes used to represent the actual entry.
                                    For strings this is just the string length
                                    while for integers it is 1, 2, 3, 4, 8 or
                                    0 (for 4 bit immediate) depending on the
                                    number range. */
    // prevlen 占用的字节数 + encoding 字段占用的字节数
    unsigned int headersize;     /* prevrawlensize + lensize. */
    // 编码方式
    unsigned char encoding;      /* Set to ZIP_STR_* or ZIP_INT_* depending on
                                    the entry encoding. However for 4 bits
                                    immediate integers this can assume a range
                                    of values and must be range-checked. */
    // 节点的首地址
    unsigned char *p;            /* Pointer to the very start of the entry, that
                                    is, this points to prev-entry-len field. */
} zlentry;

zipSaveInteger

  • 函数功能:传入新插入节点存储数据空间的地址,以及数值编码对应的值,对 encoding 做判断,将数值插入到该空间。
  • 参数:
    • p:节点存储数据的地址。
    • value:保存的数值。
    • encoding:编码方式。
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) {
        /* Nothing to do, the value is stored in the encoding itself. */
        // 这种编码方式,encoding 字段的低 4 字节就已经存储好了数值编码之后的 value 啦,详见 zipTryEncoding 函数。
    } else {
        assert(NULL);
    }
}

ZIPLIST_INCR_LENGTH

宏功能:修改 ziplistzllen 字段。zllen 字段用来记录 ziplist 的节点数量,类型:uint16_t,如果存在超过 2 16 − 2 2^{16} - 2 2162 个节点,则这个值设置为 2 16 − 1 2^{16} - 1 2161,这时需要遍历整个链表来获取正真的节点数量。

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

级联更新

考虑一种极端场景,在 ziplist 的 e2 节点前面插入一个新的节点 ne,元素数据长度为 254,如下图所示:

img

插入节点后 e2 的 prevlen 属性长度需要更新为 5 字节。

注意 e3 的 prevlen,插入前 e2 的长度为 253,所以 e3 的 prevlen 属性长度为 1 字节,插入新节点后,e2 的长度为 257,那么 e3 的prevlen 属性长度也要更新了,这就是级联更新。在极端情况下,e3 的后续节点也要持续更新 prevlen 属性。


在阅读过 __ziplistCascadeUpdate 的代码之后,或者说单凭感觉,我们都足以知道级联更新下的性能是非常糟糕的,而且代码复杂度也高,那么怎么解决这个问题呢?

我们先来看看为什么要使用 prevlen 这个字段?

这是因为反向遍历时,每向前跨过一个节点,都必须知道前面这个节点的长度。

既然这样,我们把每个节点长度都保存一份到节点的最后位置,反向遍历时,直接从前面一个节点的最后位置获取前一个节点的长度不就可以了嘛?而且这样每个节点都是独立的,插入或删除节点都不会有级联更新的现象。基于这种设计,Redis 作者设计另一种结构 listpack。设计 listpack 的目的是取代 ziplist,但是 ziplist 使用范围比较广,替换起来比较复杂,所以目前应用在新增的 Stream 结构中。

__ziplistCascadeUpdate

可以先看看什么是级联更新,看完这个函数可能会比较好理解一点。级联更新

  • 函数功能:级联更新节点的 prevlen 字段,包括 prevlen 字段的大小,prevlen 字段存储的值哈!

  • 参数:

    • zlziplist 的首地址。
    • p:新插入节点的后驱节点。
unsigned char *__ziplistCascadeUpdate(unsigned char *zl, unsigned char *p) {
    size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), rawlen, rawlensize; // ZIPLIST_BYTES 获取整个 ziplist 所占用的字节数
    size_t offset, noffset, extra;
    unsigned char *np;
    zlentry cur, next;

    while (p[0] != ZIP_END) { // 遍历整个 ziplist
        zipEntry(p, &cur); // 将当前节点的相关信息保存到 cur 这个结构体中,具体的字段可以到这个函数的实现那里去看看
        rawlen = cur.headersize + cur.len; // rawlen 就是整个节点占用的字节数
        rawlensize = zipStorePrevEntryLength(NULL,rawlen); // rawlensize 就是存储当前节点需要的 prevlen 字段的字节数

        /* Abort if there is no next entry. */
        if (p[rawlen] == ZIP_END) break; // 如果当前节点是 ziplist 的最后一个节点,后面都没有节点了,自然就不需要更新啦!
        zipEntry(p+rawlen, &next); // 将下一个节点的相关信息保存到 next 这个结构体中

        /* Abort when "prevlen" has not changed. */
        if (next.prevrawlen == rawlen) break; // 如果下一个节点 prevlen 字段存储的值和 当前节点所占的字节数相等,说明:当前节点的 prevlen 字段都没有更新,自然就不用往后更新其他节点啦!

        if (next.prevrawlensize < rawlensize) { // 存储当前节点需要的 prevlen 字段的字节数大于下一个节点 prevlen 字段的字节数,说明下一个节点需要扩容啦!
            offset = p-zl; // 当前节点相对于 ziplist 首地址的偏移量, realloc 可能修改原 zl 嘛,需要记录偏移量
            extra = rawlensize-next.prevrawlensize; // 需要在原来的基础上增加多少字节的空间
            zl = ziplistResize(zl,curlen+extra); // 扩容啦!!
            p = zl+offset; // 根据偏移量找到当前节点

            /* Current pointer and offset for next element. */
            np = p+rawlen; // 找到下一个节点的首地址哇!
            noffset = np-zl; // 下一个节点相对于 ziplist 首地址的偏移量

            // 当下一个节点不是尾节点的时候,我们需要更新 ziplist 的 zltail 字段的值
            if ((zl+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))) != np) {
                ZIPLIST_TAIL_OFFSET(zl) =
                    intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+extra);
            }

            // 移动数据
            memmove(np+rawlensize,
                np+next.prevrawlensize,
                curlen-noffset-next.prevrawlensize-1);
            zipStorePrevEntryLength(np,rawlen); // 更新下一个节点 prevlen 字段的值

            p += rawlen; // 更新当前节点
            curlen += extra; // 更新整个 ziplist 的长度
        } else { // 下一个节点不需要扩容
            if (next.prevrawlensize > rawlensize) {
                // 这种情况下,下一个节点的 prevlen 本来需要缩容的!但是,为了不让级联更新继续下去,这个时候强制后驱节点的 prevlen 占用的大小保持不变。
                zipStorePrevEntryLengthLarge(p+rawlen,rawlen); // 更新下一个节点的 prevlen 字段存储的值
            } else {
                zipStorePrevEntryLength(p+rawlen,rawlen); // 更新下一个节点的 prevlen 字段存储的值
            }

            // 到这里说明级联更新结束啦,因为下一个节点 prevlen 字段足够存储当前节点占用的字节数
            break;
        }
    }
    return zl; // 返回 ziplist 的首地址,这是必须的,因为传入的 zl 指针可能会被修改
}

ziplistNew

  • 函数功能:创建一个空的 ziplist
  • 返回值:空 ziplist 的首地址。
  • ZIPLIST_HEADER_SIZE#define ZIPLIST_HEADER_SIZE (sizeof(uint32_t)*2+sizeof(uint16_t))
  • ZIPLIST_END_SIZE#define ZIPLIST_END_SIZE (sizeof(uint8_t))
unsigned char *ziplistNew(void) {
    unsigned int bytes = ZIPLIST_HEADER_SIZE+ZIPLIST_END_SIZE; // zlbytes + zltail + zllen + zend
    unsigned char *zl = zmalloc(bytes); // 开辟空间
    ZIPLIST_BYTES(zl) = intrev32ifbe(bytes); // 初始化 zlbytes 字段
    ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE); // 初始化 zltail 字段
    ZIPLIST_LENGTH(zl) = 0; // 初始化 zlen 字段
    zl[bytes-1] = ZIP_END; // 初始化 zend
    return zl; // 返回空的 ziplist 啦
}

#define ZIPLIST_LENGTH(zl)      (*((uint16_t*)((zl)+sizeof(uint32_t)*2)))

ziplistPush

  • 函数功能:在 ziplist 头部或者尾部插入一个节点。
  • 参数:
    • zlziplist 的首地址,表示要在哪一个 ziplist 中插入节点。
    • s:待插入节点的数据。
    • slen:插入节点的数据的长度。
    • where:头插还是尾插。
  • 返回值: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); // 插入元素啦
}

#define ZIPLIST_ENTRY_HEAD(zl)  ((zl)+ZIPLIST_HEADER_SIZE)

#define ZIPLIST_ENTRY_TAIL(zl)  ((zl)+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl)))

ziplistDelete

  • 函数功能:删除给定的节点。
  • 参数:
    • zl:删除的节点属于哪一个 ziplist
    • p:待删除节点的二级指针。
  • 返回值:返回删除节点后 ziplist 的首地址。
unsigned char *ziplistDelete(unsigned char *zl, unsigned char **p) {
    size_t offset = *p-zl; // 要删除的节点相对于 ziplist 首地址的偏移量
    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;
}

__ziplistDelete

  • 函数功能:传入 ziplist 的首地址,一个节点的地址,向后删除 num 个节点。
  • 参数:
    • zlziplist 的首地址。
    • 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;

    zipEntry(p, &first); // 将节点 p 的相关信息保存到 first 结构体中
    for (i = 0; p[0] != ZIP_END && i < num; i++) { // 根据 num 找到最后一个可删除节点的首地址,当节点 p 后面的节点数不足 num,就是删除节点 p 之后的所有节点啦!
        p += zipRawEntryLength(p); // 依次跳过每个待删除的节点, p 最后指向的应该是最后一个需要删除节点的下一个节点。
        deleted++; // 删除的节点数
    }

    totlen = p-first.p; // 总共需要删除的字节数
    if (totlen > 0) { // 有需要删除的节点
        if (p[0] != ZIP_END) { // 最后一个需要删除节点的下一个节点不是 zlend 
            /* 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. */
            nextdiff = zipPrevLenByteDiff(p,first.prevrawlen); // 最后一个需要删除节点的下一个节点的 prevlen 字段是否能存储第一个需要删除节点的前驱节点所占的字节数。

            /* 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 -= nextdiff; // 适配节点 p 的 prevlen 字段所占的字节数
            zipStorePrevEntryLength(p,first.prevrawlen); // 修改节点 p 的 prevlen 字段存储的值

            // 更新 ziplist 的 zltail 字段
            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. */
            zipEntry(p, &tail); // 将节点 p(需要删除的最后一个节点的下一个节点) 的相关信息保存到 tail 结构体中
            if (p[tail.headersize+tail.len] != ZIP_END) { // 节点 p 之后还有其他的节点,更新 zltail 字段的时候就要加上 nextdiff
                ZIPLIST_TAIL_OFFSET(zl) =
                   intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+nextdiff);
            }

            // 移动指针 p 之后的所有数据,不包括那个 zlend 哈
            memmove(first.p,p,
                intrev32ifbe(ZIPLIST_BYTES(zl))-(p-zl)-1);
        } else { // 说明节点 p 之后的所有节点都需要删除(包括节点 p)
            /* The entire tail was deleted. No need to move memory. */
            // 这样的话,我们就不需要移动数据啦,直接修改 zltail 字段就行啦
            ZIPLIST_TAIL_OFFSET(zl) =
                intrev32ifbe((first.p-zl)-first.prevrawlen);
        }

        /* Resize and update length */
        offset = first.p-zl; // 第一个需要删除节点相对于 ziplist 首地址的偏移量
        zl = ziplistResize(zl, intrev32ifbe(ZIPLIST_BYTES(zl))-totlen+nextdiff); // 重新开辟空间,我们已经移动了数据,realloc 符保留所有有效的数据,并且我们放弃了原来空间的继续使用。
        ZIPLIST_INCR_LENGTH(zl,-deleted); // 修改 zllen 字段
        p = zl+offset; // 让 p 重新指向最后一个需要删除节点的下一个节点

        /* 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) // nextdiff 不等于 0 进行级联更新
            zl = __ziplistCascadeUpdate(zl,p);
    }
    return zl; // 返回新的 ziplist 的首地址
}

总结

ziplist 每插入一个新节点,需要进行两次内存拷贝操作:

  1. 为整个链表分配新内存空间,主要是为新节点创建空间。
  2. 将插入节点所有后驱节点后移,为插入节点腾出空间。

如果链表很长,则每次插入或删除节点时都需要进行大量的内存拷贝,这个性能是无法接受的,为了解决这个问题,我们在下一篇文章中会学到 quicklist

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1636739.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux 的静态库和动态库

本文目录 一、静态库1. 创建静态库2. 静态库的使用 二、动态库1. 为什么要引入动态库呢&#xff1f;2. 创建动态库3. 动态库的使用4. 查看可执行文件依赖的动态库 一、静态库 在编译程序的链接阶段&#xff0c;会将源码汇编生成的目标文件.o与引用到的库&#xff08;包括静态库…

2024小米SU7首批锁单用户调研报告

来源&#xff1a;电动汽车用户联盟 80%的锁单用户认为自己是米粉&#xff0c;64%的用户拥有10个以上米家生态产品&#xff0c; 使用小米手机的比例为67%&#xff0c;使用苹果手机的比例为47% 2. 81%的用户为90后&#xff0c;均龄31岁&#xff0c;未婚者和已婚无孩者占比63%&am…

接口测试 - postman

文章目录 一、接口1.接口的类型2. 接口测试3. 接口测试流程4. 接口测试用例1. 测试用例单接口测试用例-登录案例 二、HTTP协议1. HTTP请求2. HTTP响应 三、postman1. 界面导航说明导入 导出用例集 Get请求和Post请求的区别:2.postman环境变量和全局变量3. postman 请求前置脚本…

Java微服务分布式分库分表ShardingSphere - ShardingSphere-JDBC

&#x1f339;作者主页&#xff1a;青花锁 &#x1f339;简介&#xff1a;Java领域优质创作者&#x1f3c6;、Java微服务架构公号作者&#x1f604; &#x1f339;简历模板、学习资料、面试题库、技术互助 &#x1f339;文末获取联系方式 &#x1f4dd; 往期热门专栏回顾 专栏…

【算法刷题 | 贪心算法05】4.27(K次取反后最大化的数组和、加油站)

文章目录 8.K次取反后最大化的数组和8.1题目8.2解法&#xff1a;贪心8.2.1贪心思路8.2.2代码实现 9.加油站9.1题目9.2解法&#xff1a;贪心9.2.1贪心思路9.2.2代码实现 8.K次取反后最大化的数组和 8.1题目 给你一个整数数组 nums 和一个整数 k &#xff0c;按以下方法修改该数…

基于EBAZ4205矿板的图像处理:03使用VIO调试输出HDMI视频图像

基于EBAZ4205矿板的图像处理&#xff1a;03使用VIO调试输出HDMI视频图像 在zynq调试时VIO是真的方便&#xff0c;特此写一篇博客记录一下 先看效果 项目简介 下面是我的BD设计&#xff0c;vtc用于生成时序&#xff0c;注意&#xff0c;2021.2的vivado的vtcIP是v6.2版本&…

【算法】【贪心算法】【leetcode】870. 优势洗牌

题目地址&#xff1a;https://leetcode.cn/problems/advantage-shuffle/description/ 题目描述&#xff1a; 给定两个长度相等的数组 nums1 和 nums2&#xff0c;nums1 相对于 nums2 的优势可以用满足 nums1[i] > nums2[i] 的索引 i 的数目来描述。 返回 nums1 的任意排列&…

C++入门基础(二)

目录 缺省参数缺省参数概念缺省参数分类全缺省参数半缺省参数声明与定义分离 缺省参数的应用 函数重载函数重载概念例子1 参数类型不同例子2 参数的个数不同例子3 参数的顺序不同 C支持函数重载的原理--名字修饰(name Mangling) 感谢各位大佬对我的支持,如果我的文章对你有用,欢…

nginx+Tomcat动静分离

本⽂的动静分离主要是通过nginxtomcat来实现&#xff0c;其中nginx处理图⽚、html等静态的⽂ 件&#xff0c;tomcat处理jsp、do等动态⽂件. 实验环境 192.168.200.133 nginx反向代理 192.168.200.129 static 192.168.200.130 dynamic 步骤 修改三台主机名 [rootadmin ~]#…

《Redis使用手册之列表》

《Redis使用手册之列表》 目录 **《Redis使用手册之列表》****LPUSH&#xff1a;将元素推入列表左端****LPUSHX、RPUSHX&#xff1a;只对已存在的列表执行推入操作****LPOP&#xff1a;弹出列表最左端的元素****RPOP&#xff1a;弹出列表最右端的元素****RPOPLPUSH&#xff1a;…

【C语言刷题系列】删除公共元素

目录 一、问题描述 二、解题思路 三、源代码实现 解决方案一&#xff1a;拷贝到临时数组 解决方案二&#xff1a;直接打印 个人主页&#xff1a; 倔强的石头的博客 系列专栏 &#xff1a;C语言指南 C语言刷题系列 一、问题描述 二、解题思路 第一种方法&…

线上告警炸锅!FastJson 又立功了。。

前段时间新增一个特别简单的功能&#xff0c;晚上上线前review代码时想到公司拼搏进取的价值观临时加一行log日志&#xff0c;觉得就一行简单的日志基本上没啥问题&#xff0c;结果刚上完线后一堆报警&#xff0c;赶紧回滚了代码&#xff0c;找到问题删除了添加日志的代码&…

3.C++动态内存管理(超全)

目录 1 .C/C 内存分布 2. C语言中动态内存管理方式&#xff1a;malloc/calloc/realloc/free 3. C内存管理方式 3.1 new/delete操作内置类型 3.2 new和delete操作自定义类型 3.3 operator new函数 3.4 定位new表达式(placement-new) &#xff08;了解&#xff09; 4. 常…

iA Writer for Mac:简洁强大的写作软件

在追求高效写作的今天&#xff0c;iA Writer for Mac凭借其简洁而强大的功能&#xff0c;成为了许多作家、记者和学生的首选工具。这款专为Mac用户打造的写作软件&#xff0c;以其独特的设计理念和实用功能&#xff0c;助你轻松打造高质量的文章。 iA Writer for Mac v7.1.2中文…

C语言(操作符)1

Hi~&#xff01;这里是奋斗的小羊&#xff0c;很荣幸各位能阅读我的文章&#xff0c;诚请评论指点&#xff0c;关注收藏&#xff0c;欢迎欢迎~~ &#x1f4a5;个人主页&#xff1a;小羊在奋斗 &#x1f4a5;所属专栏&#xff1a;C语言 本系列文章为个人学习笔记&#x…

vue2迁移到vue3,v-model的调整

项目从vue2迁移到vue3&#xff0c;v-model不能再使用了&#xff0c;需要如何调整&#xff1f; 下面只提示变化最小的迁移&#xff0c;不赘述vue2和vue3中的常规写法。 vue2迁移到vue3&#xff0c;往往不想去调整之前的代码&#xff0c;以下就使用改动较小的方案进行调整。 I…

Java并发编程面试问题与答案

1. 什么是线程安全&#xff1f; 答&#xff1a; 线程安全意味着多个线程可以同时访问一个类的实例而不引起任何问题或不一致的结果。线程安全的代码会通过同步机制来确保所有线程都能正确地访问共享资源。 2. 解释Java中的synchronized关键字。 答&#xff1a; synchronized…

Q1营收稳健增长,云从科技如何在“百模大战”的险中求稳?

自从迈入大模型时代&#xff0c;AI行业可谓“一天一个样”。越来越多的企业涌现&#xff0c;舆论热议从未断绝。 但就像所有技术必须经历的那些考验&#xff0c;在现实尺度下&#xff0c;AI顺利走进商业化世界&#xff0c;仍然是少部分玩家掌握的稀缺能力。个中原因不尽相同&a…

解决“未能正确加载QtVsToolPackage包“问题

今天&#xff0c;在使用VS2019Qt插件时&#xff0c;弹出"未能正确加载QtVsToolPackage包"错误&#xff0c;如图(1.1)所示&#xff1a; 图(1.1) 报"未能正确加载QtVsToolsPackage包"错误 出现这种现象的原因是: qt-vsaddin升级失败或者版本不兼容&#xff0…

动手学深度学习3.6 softmax回归的从零开始实现-笔记练习(PyTorch)

以下内容为结合李沐老师的课程和教材补充的学习笔记&#xff0c;以及对课后练习的一些思考&#xff0c;自留回顾&#xff0c;也供同学之人交流参考。 本节课程地址&#xff1a;Softmax 回归从零开始实现_哔哩哔哩_bilibili 本节教材地址&#xff1a;3.6. softmax回归的从零开…