Redis源码——压缩列表

压缩列表ziplist本质上就是一个字节数组,是Redis为了节约内存而设计的一种线性数据结构,可以包含多个元素,每个元素可以是一个字节数组或一个整数。Redis的有序集合、散列和列表都直接或者间接使用了压缩列表。当有序集合或散列表的元素个数比较少,且元素都是短字符串时,Redis便使用压缩列表作为其底层数据存储结构。列表使用快速链表(quicklist)数据结构存储,而快速链表就是双向链表与压缩列表的组合。

例如,使用如下命令创建一个散列键并查看其编码。

    127.0.0.1:6379> hmset person name zhangsan gender 1 age 22OK127.0.0.1:6379> object encoding person"ziplist"

本章将从源码层次详细介绍压缩列表的存储结构及基本操作。

压缩列表的存储结构

Redis使用字节数组表示一个压缩列表,压缩列表结构示意如下图所示:

各字段的含义如下:

  • zlbytes:压缩列表的字节长度,占4个字节,因此压缩列表最多有2^{32}-1个字节。
  • zltail:压缩列表尾元素相对于压缩列表起始地址的偏移量,占4个字节。
  • zllen:压缩列表的元素个数,占2个字节。zllen无法存储元素个数超过65535(2^{16}-1)的压缩列表,必须遍历整个压缩列表才能获取到元素个数。
  • entryX:压缩列表存储的元素,可以是字节数组或者整数,长度不限。entry的编码结构将在后面详细介绍。
  • zlend:压缩列表的结尾,占1个字节,恒为0xFF。

假设char * zl指向压缩列表首地址,Redis可通过以下宏定义实现压缩列表各个字段的存取操作。

/* zl返回zlbytes的总字节数。 */
#define ZIPLIST_BYTES(zl)       (*((uint32_t*)(zl)))/* zl+4返回zltail的偏移量。 */
#define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t))))/* 返回ziplist的长度,如果无法确定长度,则返回UINT16_MAX。 */
#define ZIPLIST_LENGTH(zl)      (*((uint16_t*)((zl)+sizeof(uint32_t)*2)))/* ziplist头部的大小:两个32位整数用于总字节数和最后一个项的偏移量。一个16位整数用于项数字段。 */
#define ZIPLIST_HEADER_SIZE     (sizeof(uint32_t)*2+sizeof(uint16_t))/* 返回zlend的大小,只有一个字节。 */
#define ZIPLIST_END_SIZE        (sizeof(uint8_t))/* 返回ziplist的第一个条目的指针。 */
#define ZIPLIST_ENTRY_HEAD(zl)  ((zl)+ZIPLIST_HEADER_SIZE)/* 使用ziplist头部中的最后一个条目偏移量,返回ziplist的最后一个条目的指针。 */
#define ZIPLIST_ENTRY_TAIL(zl)  ((zl)+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl)))/* 返回ziplist的最后一个字节的指针,即"end of ziplist" FF条目的末尾。 */
#define ZIPLIST_ENTRY_END(zl)   ((zl)+intrev32ifbe(ZIPLIST_BYTES(zl))-ZIPLIST_END_SIZE)

其中头部是zlbytes+zltail+zllen,一共占用了4+4+2,即ZIPLIST_HEADER_SIZE。压缩列表中第一个元素的指针偏移量是zl+ZIPLIST_HEADER_SIZE。

了解了压缩列表的基本结构,我们可以很容易地获得压缩列表的字节长度、元素个数等,那么如何遍历压缩列表呢?对于任意一个元素,我们如何判断其存储的是什么类型呢?我们又如何获取字节数组的长度呢?

回答这些问题之前,需要先了解压缩列表元素的编码结构,如下所示:

previous_entry_length字段表示前一个元素的字节长度,占1个或者5个字节,当前一个元素的长度小于254字节时,用1个字节表示;当前一个元素的长度大于或等于254字节时,用5个字节来表示。而此时previous_entry_length字段的第1个字节是固定的0xFE,后面4个字节才真正表示前一个元素的长度。假设已知当前元素的首地址为p,那么p-previous_entry_length就是前一个元素的首地址,从而实现压缩列表从尾到头的遍历。

encoding字段表示当前元素的编码,即content字段存储的数据类型(整数或者字节数组),数据内容存储在content字段。为了节约内存,encoding字段同样长度可变。压缩列表元素的编码如下表所示:

可以看出,根据encoding字段第1个字节的前2位,可以判断content字段存储的是整数或者字节数组(及其最大长度)。当content存储的是字节数组时,后续字节标识字节数组的实际长度;当content存储的是整数时,可根据第3、第4位判断整数的具体类型。而当encoding字段标识当前元素存储的是0~12的立即数时,数据直接存储在encoding字段的最后4位,此时没有content字段。参照encoding字段的编码表格,Redis预定义了以下常量对应encoding字段的各编码类型:

#define ZIP_STR_06B (0 << 6)
#define ZIP_STR_14B (1 << 6)
#define ZIP_STR_32B (2 << 6)
#define ZIP_INT_16B (0xc0 | 0<<4)
#define ZIP_INT_32B (0xc0 | 1<<4)
#define ZIP_INT_64B (0xc0 | 2<<4)
#define ZIP_INT_24B (0xc0 | 3<<4)
#define ZIP_INT_8B 0xfe

结构体

我们发现对于压缩列表的任意元素,获取前一个元素的长度、判断存储的数据类型、获取数据内容都需要经过复杂的解码运算。解码后的结果应该被缓存起来,为此定义了结构体zlentry,用于表示解码后的压缩列表元素。

/* 我们使用这个函数来接收关于ziplist条目的信息。* 注意,这些数据并不是这样编码的,这只是我们通过一个函数填充的以便更轻松地操作的数据。 */
typedef struct zlentry {unsigned int prevrawlensize; /* 编码前一个条目长度所使用的字节数 */unsigned int prevrawlen;     /* 前一个条目长度 */unsigned int lensize;        /* 编码条目类型/长度所使用的字节数。例如,字符串有1、2或5个字节的头部。整数始终使用一个字节。*/unsigned int len;            /* 表示实际条目的字节数。对于字符串,这只是字符串的长度,而对于整数,取决于数值范围,可以是1、2、3、4、8或0(对于4位立即整数)。 */unsigned int headersize;     /* prevrawlensize + lensize */unsigned char encoding;      /* 根据条目的编码,设置为ZIP_STR_*或ZIP_INT_*。然而,对于4位立即整数,它可以假设一组值并需要进行范围检查。 */unsigned char *p;            /* 指向条目开头的指针,即指向前条目长度字段。 */
} zlentry;

回顾压缩列表元素的编码结构,可变因素实际上不止3个:previous_entry_length字段的长度(prevrawlensize)、previous_entry_length字段存储的内容(prevrawlen)、encoding字段的长度(lensize)、encoding字段的内容(len表示元素数据内容的长度,encoding表示数据类型)和当前元素首地址(p);而headersize则表示当前元素的首部长度,即previous_entry_length字段长度与encoding字段长度之和。

函数zipEntry用来解码压缩列表的元素,存储于zlentry结构体。

/* 填充一个关于条目的信息的结构体。* 此函数是下面那个函数的"不安全"替代方案。* 一般情况下,返回ziplist中元素指针的所有函数都会断言这个元素是有效的,因此可以自由使用。* 一般情况下,ziplistGet等函数假设输入指针已经经过验证(因为它是另一个函数的返回值)。*/
static inline void zipEntry(unsigned char *p, zlentry *e) {ZIP_DECODE_PREVLEN(p, e->prevrawlensize, e->prevrawlen);ZIP_ENTRY_ENCODING(p + e->prevrawlensize, e->encoding);ZIP_DECODE_LENGTH(p + e->prevrawlensize, e->encoding, e->lensize, e->len);assert(e->lensize != 0); /* 检查编码是否有效。*/e->headersize = e->prevrawlensize + e->lensize;e->p = p;
}

解码主要可以分为以下两个步骤。

1)解码previous_entry_length字段,此时入参ptr指向元素首地址。

#define ZIP_END 255         /* 特殊的 "ziplist结尾" 标记值。 */
#define ZIP_BIG_PREVLEN 254 /* ZIP_BIG_PREVLEN - 1 是前一个条目的最大长度,用于表示每个条目的"prevlen"字段前缀,如果长度大于等于254字节,则使用FE AA BB CC DD表示,其中AA BB CC DD是一个无符号整数,使用4个字节表示前一个条目的长度。 *//* * 返回用于编码上一个条目的长度的字节数。 * 长度通过将变量 'prevlensize' 设置为特定值来返回。*/
#define ZIP_DECODE_PREVLENSIZE(ptr, prevlensize) do {                          \if ((ptr)[0] < ZIP_BIG_PREVLEN) {                                          \(prevlensize) = 1;                                                     \} else {                                                                   \(prevlensize) = 5;                                                     \}                                                                          \
} while(0)/* 返回前一个元素的长度以及用于编码前一个元素长度的字节数。* 'ptr' 必须指向一个元素的前一个元素长度的 prevlen 前缀(用于遍历元素向后导航)。* 前一个元素的长度存储在 'prevlen' 中,编码前一个元素长度所需的字节数存储在 'prevlensize' 中。 */
#define ZIP_DECODE_PREVLEN(ptr, prevlensize, prevlen) do {                     \ZIP_DECODE_PREVLENSIZE(ptr, prevlensize);                                  \if ((prevlensize) == 1) {                                                  \(prevlen) = (ptr)[0];                                                  \} else { /* prevlensize == 5 */                                            \(prevlen) = ((ptr)[4] << 24) |                                         \((ptr)[3] << 16) |                                         \((ptr)[2] <<  8) |                                         \((ptr)[1]);                                                \}                                                                          \
} while(0)

2)解码encoding字段逻辑,此时入参ptr指向元素首地址偏移previous_entry_length字段长度的位置。

#define ZIP_STR_MASK 0xc0/* 从指向 'ptr' 的字节中提取编码,并将其设置为 'zlentry' 结构体中的 'encoding' 字段 */
#define ZIP_ENTRY_ENCODING(ptr, encoding) do {  \(encoding) = ((ptr)[0]); \if ((encoding) < ZIP_STR_MASK) (encoding) &= ZIP_STR_MASK; \
} while(0)/* 
* 解码'ptr'中的条目编码类型和数据长度(字符串长度为字符串,整数条目使用的字节数)。
* 'encoding'变量为输入参数,调用者提取,'lensize'变量将保存用于编码条目长度的字节数,
* 'len'变量将保存条目长度。在无效编码错误的情况下,lensize被设置为0。
*/
#define ZIP_DECODE_LENGTH(ptr, encoding, lensize, len) do {                    \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) = ((uint32_t)(ptr)[1] << 24) |                               \((uint32_t)(ptr)[2] << 16) |                               \((uint32_t)(ptr)[3] <<  8) |                               \((uint32_t)(ptr)[4]);                                      \} else {                                                               \(lensize) = 0; /* 编码错误,应该在之前的代码中被覆盖 */         \(len) = 0;      /* ZIP_ASSERT_ENCODING / zipEncodingLenSize,或者 */ \/* 在此宏之后将lensize与0匹配 */                 \}                                                                      \} else {                                                                   \(lensize) = 1;                                                         \if ((encoding) == ZIP_INT_8B)  (len) = 1;                              \else if ((encoding) == ZIP_INT_16B) (len) = 2;                         \else if ((encoding) == ZIP_INT_24B) (len) = 3;                         \else if ((encoding) == ZIP_INT_32B) (len) = 4;                         \else if ((encoding) == ZIP_INT_64B) (len) = 8;                         \else if (encoding >= ZIP_INT_IMM_MIN && encoding <= ZIP_INT_IMM_MAX)   \(len) = 0; /* 4位立即值 */                                        \else                                                                   \(lensize) = (len) = 0; /* 编码错误 */                             \}                                                                          \
} while(0)

字节数组只根据ptr[0]的前2个比特即可判断类型,而判断整数类型需要ptr[0]的前4个比特,代码如下。

/*
* 返回由'encoding'编码的整数所需的字节数
*/
static inline 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;}if (encoding >= ZIP_INT_IMM_MIN && encoding <= ZIP_INT_IMM_MAX)return 0; /* 4位立即值 *//* 编码错误,通过之前的ZIP_ASSERT_ENCODING调用已覆盖 */redis_unreachable();return 0;
}

基本操作

本节主要介绍压缩列表的基本操作,包括创建压缩列表、插入元素、删除元素及遍历压缩列表,让读者从源码层次进一步理解压缩列表。

创建压缩列表

创建压缩列表的API定义如下,函数无输入参数,返回参数为压缩列表首地址。

unsigned char *ziplistNew(void);

创建空的压缩列表,只需要分配初始存储空间11(4+4+2+1)个字节,并对zlbytes、zltail、zllen和zlend字段初始化即可。

/* Create a new empty ziplist. */
unsigned char *ziplistNew(void) {//ZIPLIST_HEADER_SIZE = zlbytes + zltail + zllen;unsigned int bytes = ZIPLIST_HEADER_SIZE+ZIPLIST_END_SIZE;unsigned char *zl = zmalloc(bytes);ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);ZIPLIST_LENGTH(zl) = 0;zl[bytes-1] = ZIP_END; //结尾标识0XFFreturn zl;
}

插入元素

压缩列表插入元素的API定义如下,函数输入参数zl表示压缩列表首地址,p指向元素插入位置,s表示数据内容,slen表示数据长度,返回参数为压缩列表首地址。

/* 
* 在指定位置 "p" 插入一个键值对。
* 
* 参数:
* - zl: 待操作的 ziplist。
* - p: 待插入的位置。
* - s: 键值对的键。
* - slen: 键值对的键的长度。
*
* 返回值:
* - 插入后的 ziplist。
*/
unsigned char *ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {return __ziplistInsert(zl,p,s,slen);
}/* * 在指定位置插入项目。* * @param zl 待插入的ziplist* @param p 插入位置的指针* @param s 待插入的字符串* @param slen 待插入字符串的长度* @return 更新后的ziplist*/
unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), reqlen, newlen;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);} else {unsigned char *ptail = ZIPLIST_ENTRY_TAIL(zl);if (ptail[0] != ZIP_END) {prevlen = zipRawEntryLengthSafe(zl, curlen, ptail);}}/* 查看是否可以对条目进行编码 */if (zipTryEncoding(s,slen,&value,&encoding)) {/* 将encoding设置为相应的整数编码 */reqlen = zipIntSize(encoding);} else {/* encoding不变,但是zipStoreEntryEncoding将使用字符串长度来确定如何编码。 */reqlen = slen;}/* 我们需要为之前的记录长度和有效负载长度腾出空间。 */reqlen += zipStorePrevEntryLength(NULL,prevlen);reqlen += zipStoreEntryEncoding(NULL,encoding,slen);/* 如果插入位置不等于尾部,则需要确保下一个条目的prevlen字段可以容纳此条目的长度。 */int forcelarge = 0;nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0;if (nextdiff == -4 && reqlen < 4) {nextdiff = 0;forcelarge = 1;}/* 存储realloc可能改变ziplist地址的情况下的地址。 */offset = p-zl;newlen = curlen+reqlen+nextdiff;zl = ziplistResize(zl,newlen);p = zl+offset;/* 应用必要的内存移动并更新尾部偏移量。 */if (p[0] != ZIP_END) {/* 减少一个是由于ZIP_END字节 */memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff);/* 在下一个条目中编码此条目的raw长度。 */if (forcelarge)zipStorePrevEntryLengthLarge(p+reqlen,reqlen);elsezipStorePrevEntryLength(p+reqlen,reqlen);/* 更新尾部偏移量。 */ZIPLIST_TAIL_OFFSET(zl) =intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+reqlen);/* 当尾部包含多于一个条目时,我们还需要考虑"nextdiff"。否则,prevlen大小的变化对尾部偏移量没有影响。 */assert(zipEntrySafe(zl, newlen, p+reqlen, &tail, 1));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时,下一个条目的原始长度发生变化,因此需要在ziplist中进行更新。 */if (nextdiff != 0) {offset = p-zl;zl = __ziplistCascadeUpdate(zl,p+reqlen);p = zl+offset;}/* 写入条目 */p += zipStorePrevEntryLength(p,prevlen);p += zipStoreEntryEncoding(p,encoding,slen);if (ZIP_IS_STR(encoding)) {memcpy(p,s,slen);} else {zipSaveInteger(p,value,encoding);}ZIPLIST_INCR_LENGTH(zl,1);return zl;
}

插入元素可以简要分为3个步骤:① 将元素内容编码;② 重新分配空间;③ 复制数据。下面分别讲解每个步骤的实现逻辑。

1.编码

编码即计算previous_entry_length字段、encoding字段和content字段的内容。那么如何获取前一个元素的长度呢?此时就需要根据元素的插入位置分情况讨论。插入元素位置示意如下所示:

1)当压缩列表为空、插入位置为P0时,不存在前一个元素,即前一个元素的长度为0。

2)当插入位置为P1时,需要获取entryX元素的长度,而entryX+1元素的previous_entry_length字段存储的就是entryX元素的长度,比较容易获取。

3)当插入位置为P2时,需要获取entryN元素的长度,entryN是压缩列表的尾元素,计算元素长度时需要将其3个字段长度相加,函数实现如下。

/* 
* 返回由指针 'p' 指向的条目使用的总字节数。 */
static inline unsigned int zipRawEntryLengthSafe(unsigned char* zl, size_t zlbytes, unsigned char *p) {zlentry e;assert(zipEntrySafe(zl, zlbytes, p, &e, 0));return e.headersize + e.len;
}/*** 将结构体填充为关于条目的所有信息。** 这个函数是安全使用的,即使是指向不受信任的指针,也能确保不会尝试访问ziplist负载外部的内存。* 返回1表示条目是有效的,返回0表示否则。*/
static inline int zipEntrySafe(unsigned char* zl, size_t zlbytes, unsigned char *p, zlentry *e, int validate_prevlen) {unsigned char *zlfirst = zl + ZIPLIST_HEADER_SIZE;unsigned char *zllast = zl + zlbytes - ZIPLIST_END_SIZE;
#define OUT_OF_RANGE(p) (unlikely((p) < zlfirst || (p) > zllast))/* 如果头部不可能超出ziplist范围,那么走快速路径。(最大lensize和prevrawlensize均为5字节) */if (p >= zlfirst && p + 10 < zllast) {ZIP_DECODE_PREVLEN(p, e->prevrawlensize, e->prevrawlen);ZIP_ENTRY_ENCODING(p + e->prevrawlensize, e->encoding);ZIP_DECODE_LENGTH(p + e->prevrawlensize, e->encoding, e->lensize, e->len);e->headersize = e->prevrawlensize + e->lensize;e->p = p;/* 我们没有调用ZIP_ASSERT_ENCODING,所以检查lensize是否被设置为0。 */if (unlikely(e->lensize == 0))return 0;/* 确保条目不会超出ziplist边缘 */if (OUT_OF_RANGE(p + e->headersize + e->len))return 0;/* 如果需要验证prevlen,则确保prevlen不会超出ziplist边缘 */if (validate_prevlen && OUT_OF_RANGE(p - e->prevrawlen))return 0;return 1;}/* 确保指针不会超出ziplist的边缘 */if (OUT_OF_RANGE(p))return 0;/* 确保编码的prevlen头部不会超出分配范围 */ZIP_DECODE_PREVLENSIZE(p, e->prevrawlensize);if (OUT_OF_RANGE(p + e->prevrawlensize))return 0;/* 确保编码的条目头是有效的。 */ZIP_ENTRY_ENCODING(p + e->prevrawlensize, e->encoding);e->lensize = zipEncodingLenSize(e->encoding);if (unlikely(e->lensize == ZIP_ENCODING_SIZE_INVALID))return 0;/* 确保编码的条目头不会超出分配范围 */if (OUT_OF_RANGE(p + e->prevrawlensize + e->lensize))return 0;/* 解码prevlen和条目长度头。 */ZIP_DECODE_PREVLEN(p, e->prevrawlensize, e->prevrawlen);ZIP_DECODE_LENGTH(p + e->prevrawlensize, e->encoding, e->lensize, e->len);e->headersize = e->prevrawlensize + e->lensize;/* 确保条目不会超出ziplist边缘 */if (OUT_OF_RANGE(p + e->headersize + e->len))return 0;/* 如果需要验证prevlen,则确保prevlen不会超出ziplist边缘 */if (validate_prevlen && OUT_OF_RANGE(p - e->prevrawlen))return 0;e->p = p;return 1;
#undef OUT_OF_RANGE
}

encoding字段标识的是当前元素存储的数据类型和数据长度。编码时首先尝试将数据内容解析为整数,如果解析成功,则按照压缩列表整数类型编码存储;如果解析失败,则按照压缩列表字节数组类型编码存储。

    /* 查看是否可以对条目进行编码 */if (zipTryEncoding(s,slen,&value,&encoding)) {/* 将encoding设置为相应的整数编码 */reqlen = zipIntSize(encoding);} else {/* encoding不变,但是zipStoreEntryEncoding将使用字符串长度来确定如何编码。 */reqlen = slen;}/* 我们需要为之前的记录长度和有效负载长度腾出空间。 */reqlen += zipStorePrevEntryLength(NULL,prevlen);reqlen += zipStoreEntryEncoding(NULL,encoding,slen);

上述程序尝试按照整数解析新添加元素的数据内容,数值存储在变量value中,编码存储在变量encoding中。如果解析成功,还需要计算整数所占字节数。变量reqlen最终存储的是当前元素所需空间大小,初始赋值为元素content字段所需空间大小,再累加previous_entry_length和encoding字段所需空间大小。

重新分配空间

由于新插入了元素,压缩列表所需空间增大,因此需要重新分配存储空间。那么空间大小是不是添加元素前的压缩列表长度与新添加元素长度之和呢?并不完全是。压缩列表长度变化示意如下图所示。

插入元素前,entryX元素的长度为128字节,entryX+1元素的previous_entry_length字段占1个字节;添加元素entryNEW,元素长度为1024字节,此时entryX+1元素的previous_entry_length字段需要占5个字节,即压缩列表的长度不仅增加了1024个字节,还要加上entryX+1元素扩展的4个字节。而entryX+1元素的长度可能增加4个字节、减少4个字节或不变。

由于重新分配了空间,新元素插入的位置指针P会失效,可以预先计算好指针P相对于压缩列表首地址的偏移量,待分配空间之后再偏移即可。

    size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl));int forcelarge = 0;nextdiff = (p[0] ! = ZIP_END) ? zipPrevLenByteDiff(p, reqlen) : 0;if (nextdiff == -4 && reqlen < 4) {nextdiff = 0;forcelarge = 1;}//存储偏移量offset = p-zl;//调用realloc重新分配空间zl = ziplistResize(zl, curlen+reqlen+nextdiff);//重新偏移到插入位置Pp = zl+offset;

nextdiff与forcelarge在这里有什么用呢?分析ziplistResize函数的第二个参数,curlen表示插入元素前压缩列表的长度;reqlen表示新插入元素的长度;而nextdiff表示entryX+1元素长度的变化,取值可能为0(长度不变)、4(长度增加4)或-4(长度减少4)。我们再思考下,当nextdiff=4而reqlen<4时会发生什么呢?没错,插入元素导致压缩列表所需空间减小了,即函数ziplistResize内部调用realloc重新分配的空间小于指针zl指向的空间。我们知道realloc重新分配空间时,返回的地址可能不变(当前位置有足够的内存空间可供分配),当重新分配的空间减小时,realloc可能会将多余的空间回收,导致数据丢失。因此需要避免这种情况的发生,即重新赋值nextdiff=0,同时使用forcelarge标记这种情况。

那么,nextdiff=-4时,reqlen会小于4吗?nextdiff=-4说明插入元素之前entryX+1元素的previous_entry_length字段的长度是5字节,即entryX元素的总长度大于或等于254字节,所以entryNEW元素的previous_entry_length字段同样需要5个字节,即entryNEW元素的总长度肯定是大于5个字节的,reqlen又怎么会小于4呢?正常情况下是不会出现这种情况的,但是由于存在连锁更新(将在4.4节介绍),可能会出现nextdiff=-4但entryX元素的总长度小于254字节的情况,此时reqlen可能会小于4。

数据复制

重新分配空间之后,需要将位置P后的元素移动到指定位置,将新元素插入到位置P。我们假设entryX+1元素的长度增加4(即nextdiff=4),此时压缩列表数据复制示意如下图所示。

从上图中可以看到,位置P后的所有元素都需要移动,移动的偏移量是待插入元素entryNEW的长度,移动的数据块长度是位置P后所有元素的长度之和加上nextdiff的值,数据移动之后还需要更新entryX+1元素的previous_entry_length字段。

/* 应用必要的内存移动并更新尾部偏移量。 */if (p[0] != ZIP_END) {//为什么减1呢?zlend结束表示恒为0xFF,不需要移动memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff);//更新entryX+1元素的previous_entry_length字段if (forcelarge)//entryX+1元素的previous_entry_length字段依然占5个字节;//但是entryNEW元素的长度小于4字节zipStorePrevEntryLengthLarge(p+reqlen,reqlen);elsezipStorePrevEntryLength(p+reqlen,reqlen);//更新zltail字段ZIPLIST_TAIL_OFFSET(zl) =intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+reqlen);/* 当尾部包含多于一个条目时,我们还需要考虑"nextdiff"。否则,prevlen大小的变化对尾部偏移量没有影响。 */assert(zipEntrySafe(zl, newlen, p+reqlen, &tail, 1));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);}

第一次更新尾元素偏移量之后,为什么指向的元素可能不是尾元素呢?因为当entryX+1元素就是尾元素时,只需要更新一次尾元素的偏移量;但是当entryX+1元素不是尾元素且entryX+1元素的长度发生了改变时,尾元素偏移量还需要加上nextdiff的值。

删除元素

压缩列表删除元素的API定义如下,函数输入参数zl指向压缩列表首地址;*p指向待删除元素的首地址(参数p同时可以作为输出参数);返回参数为压缩列表首地址。

/* 删除指针 *p指向的ziplist中的单个条目。* 同时在place中更新*p,以便能够迭代ziplist,同时删除条目。 */
unsigned char *ziplistDelete(unsigned char *zl, unsigned char **p) {size_t offset = *p-zl;zl = __ziplistDelete(zl,*p,1);/* 将当前元素的指针存储在p中,因为ziplistDelete会进行realloc,可能会导致不同的"zl"指针。* 当删除方向为从后往前时,可能会删除最后一个条目,并导致"p"指向ZIP_END,因此需要检查这一点。 */*p = zl+offset;return zl;
}

ziplistDelete函数只是简单调用底层_ziplistDelete函数实现删除功能。_ziplistDelete函数可以同时删除多个元素,输入参数p指向的是首个待删除元素的地址;num表示待删除元素数目。

/* Delete "num" entries, starting at "p". Returns pointer to the ziplist. */
unsigned char *__ziplistDelete(unsigned char *zl, unsigned char *p, unsigned int num) {unsigned int i, totlen, deleted = 0;  // 定义变量i, totlen, deleted并初始化size_t offset;  // 定义变量offsetint nextdiff = 0;  // 定义变量nextdiff并初始化zlentry first, tail;  // 定义变量first和tailsize_t zlbytes = intrev32ifbe(ZIPLIST_BYTES(zl));  // 获取ziplist的大小并赋值给zlbyteszipEntry(p, &first);  // 获取p所在entry的信息并赋值给firstfor (i = 0; p[0] != ZIP_END && i < num; i++) {  // 循环删除entryp += zipRawEntryLengthSafe(zl, zlbytes, p);  // 移动到下一个待删除的entrydeleted++;  // 删除的entry数量加1}assert(p >= first.p);  // 断言p大于等于first的起始位置totlen = p-first.p;  // 计算待删除的entry总长度if (totlen > 0) {  // 如果删除的entry总长度大于0uint32_t set_tail;  // 定义变量set_tailif (p[0] != ZIP_END) {  // 如果下一个entry不是ZIP_ENDnextdiff = zipPrevLenByteDiff(p,first.prevrawlen);  // 计算prevlen字节大小的变化量p -= nextdiff;  // 移动到上一个entry的位置assert(p >= first.p && p<zl+zlbytes-1);  // 断言p在正确的位置上zipStorePrevEntryLength(p,first.prevrawlen);  // 在上一个entry中存储prevlen的长度set_tail = intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))-totlen;  // 计算更新后的tail偏移量if (p[tail.headersize+tail.len] != ZIP_END) {set_tail += nextdiff;  // 如果下一个entry的长度发生了变化,更新tail偏移量}size_t bytes_to_move = zlbytes-(p-zl)-1;  // 计算需要移动的字节数memmove(first.p,p,bytes_to_move);  // 将内存中的数据向前移动} else {set_tail = (first.p-zl)-first.prevrawlen;  // 尾部已经被删除,不需要移动内存}zlbytes -= totlen - nextdiff;  // 更新ziplist的剩余大小zl = ziplistResize(zl, zlbytes);  // 调整ziplist的大小p = zl+offset;  // 更新p的位置ZIPLIST_INCR_LENGTH(zl,-deleted);  // 更新ziplist中的记录数assert(set_tail <= zlbytes - ZIPLIST_END_SIZE);  // 断言tail偏移量在有效范围内ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(set_tail);  // 更新tail偏移量if (nextdiff != 0) {zl = __ziplistCascadeUpdate(zl,p);  // 如果nextdiff不等于0,递归更新下一个entry的长度}}return zl;  // 返回ziplist的指针
}

删除元素同样可以简要分为三个步骤:① 计算待删除元素的总长度;② 数据复制;③ 重新分配空间。下面分别讨论每个步骤的实现逻辑。

1)计算待删除元素的总长度。

    //解码第一个待删除元素zipEntry(p, &first);//遍历所有待删除元素,同时指针p向后偏移for (i = 0; p[0] ! = ZIP_END && i < num; i++) {p += zipRawEntryLength(p);deleted++;}//totlen即为待删除元素总长度totlen = p-first.p;

2)数据复制。第1步完成之后,指针first与指针p之间的元素都是待删除的。当指针p恰好指向zlend字段时,不再需要复制数据,只需要更新尾节点的偏移量即可。下面分析另一种情况,即指针p指向的是某一个元素,而不是zlend字段。

删除元素时,压缩列表所需空间减小,减小的量是否仅为待删除元素的总长度呢?答案同样是否定的。举个简单的例子,下图是经过第1步之后的压缩列表示意。

删除元素entryX+1到元素entryN-1之间的N-X-1个元素,元素entryN-1的长度为12字节,因此元素entryN的previous_entry_length字段占1个字节;删除这些元素之后,entryX成为了entryN的前一个元素,元素entryX的长度为512字节,因此元素entryN的previous_entry_length字段需要占5个字节,即删除元素之后的压缩列表的总长度还与元素entryN长度的变化量有关。

    //计算元素entryN长度的变化量nextdiff = zipPrevLenByteDiff(p, first.prevrawlen);//更新元素entryN的previous_entry_length字段p -= nextdiff;zipStorePrevEntryLength(p, first.prevrawlen);//更新zltailZIPLIST_TAIL_OFFSET(zl) =intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))-totlen);zipEntry(p, &tail);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);

与之前更新zltail字段的操作相同,当entryN元素就是尾元素时,只需要更新一次尾元素的偏移量;但是当entryN元素不是尾元素且entryN元素的长度发生了改变时,尾元素偏移量还需要加上nextdiff的值。

3)重新分配空间。

    offset = first.p-zl;zl = ziplistResize(zl, intrev32ifbe(ZIPLIST_BYTES(zl))-totlen+nextdiff);p = zl+offset;ZIPLIST_INCR_LENGTH(zl, -deleted);

调用ziplistResize函数重新分配空间时,如果重新分配的空间小于指针zl指向的空间时,可能会出现问题。而删除元素时,压缩列表的长度肯定是减小的。因为删除元素时,先复制数据,再重新分配空间,即调用ziplistResize函数时,多余的那部分空间存储的数据已经被复制了,此时回收这部分空间并不会造成数据丢失。

遍历压缩列表

遍历就是从头到尾(后向遍历)或者从尾到头(前向遍历)访问压缩列表中的每个元素。压缩列表的遍历API定义如下,函数输入参数zl指向压缩列表首地址,p指向当前访问元素的首地址;ziplistNext函数返回后一个元素的首地址,ziplistPrev返回前一个元素的首地址。

    //后向遍历unsigned char *ziplistNext(unsigned char *zl, unsigned char *p);//前向遍历unsigned char *ziplistPrev(unsigned char *zl, unsigned char *p);

压缩列表每个元素的previous_entry_length字段存储的是前一个元素的长度,因此压缩列表的前向遍历相对简单,表达式p-previous_entry_length即可获取前一个元素的首地址,这里不做详述。后向遍历时,需要解码当前元素,计算当前元素的长度,才能获取后一个元素首地址;ziplistNext函数实现如下。

/* 返回ziplist中的下一个元素的指针。** zl是ziplist的指针* p是当前元素的指针** 返回下一个元素的指针,如果没有下一个元素则返回NULL。 */
unsigned char *ziplistNext(unsigned char *zl, unsigned char *p) {((void) zl);size_t zlbytes = intrev32ifbe(ZIPLIST_BYTES(zl));/* "p"可能等于ZIP_END,这是由ziplistDelete引起的,此时应该返回NULL。* 否则,当下一个元素是ZIP_END时(即没有下一个元素),应该返回NULL。 */if (p[0] == ZIP_END) {return NULL;}p += zipRawEntryLength(p);if (p[0] == ZIP_END) {return NULL;}zipAssertValidEntry(zl, zlbytes, p);return p;
}

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

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

相关文章

项目经验简单总结

引擎 unity 2020 语言 C# lua python(用于工具链) java (用于SDK对接) js&#xff08;PC WEB SDK对接&#xff09; 编辑器 VS VSCODE IDEA eclipse 项目开发模块规划分 主项目工程&#xff0c;UI资源项目工程&#xff0c;模型场景资源项目工程 主项目工程&#xff1a;所有的…

qt图像绘制QPainter

QPainter 以下是一些常用的 Qt::PenStyle 枚举值&#xff1a; Qt::NoPen&#xff1a;无线条。Qt::SolidLine&#xff1a;实线。Qt::DashLine&#xff1a;虚线&#xff0c;由短划线组成。Qt::DotLine&#xff1a;点线&#xff0c;由点组成。Qt::DashDotLine&#xff1a;点划线&…

炫酷鼠标悬停随机渐变文本动画效果

如图所示&#xff0c;这是一个很炫酷的鼠标悬停动画效果&#xff0c;卡片的文字随着鼠标的移动不断变化着&#xff0c;且文字的颜色伴随着渐变色跟随鼠标移动&#xff0c;中心部分是突出的LOGO效果&#xff0c;整个交互效果十分引人注目。原效果来源于 evervault.com/customers…

Head First Design Patterns - 装饰者模式

什么是装饰者模式 装饰者模式动态地将额外责任附加到对象上。对于拓展功能&#xff0c;装饰者提供子类化的弹性替代方案。 --《Head First Design Patterns》中的定义 为什么会有装饰者模式 根据上述定义&#xff0c;简单来说&#xff0c;装饰者模式就是对原有的类&#xff0c…

绝地求生:PUBG到底怎么穿?

闲游盒突然就认为这条裤子很不错&#xff0c;褶皱设计&#xff0c;版型&#xff0c;和其他饰品的搭配能力方面&#xff0c;私以为比柏林裤和悲喜白色长裤的可搭配能力要强一些。可能是因为看起来更日常化&#xff0c;在我进行尝试后发现柏林裤版型在穿鞋的时候会露出脚脖子&…

目标检测-Owo Stage-YOLOv2

文章目录 前言一、YOLOv2的网络结构和流程二、YOLOv2的创新点预处理网络结构训练 总结 前言 根据前文目标检测-One Stage-YOLOv1可以看出YOLOv1的主要缺点是&#xff1a; 和Fast-CNN相比&#xff0c;速度快&#xff0c;但精度下降。&#xff08;边框回归不加限制&#xff09;…

2023年最新版的linux运维面试题(四)

作者简介&#xff1a;一名云计算网络运维人员、每天分享网络与运维的技术与干货。 公众号&#xff1a;网络豆云计算学堂 座右铭&#xff1a;低头赶路&#xff0c;敬事如仪 个人主页&#xff1a; 网络豆的主页​​​​​ 写在前面 大家好&#xff0c;我是网络豆&#xff0…

oracle 9i10g编程艺术-读书笔记2

配置Statspack 安装Statspack需要用internal身份登陆&#xff0c;或者拥有SYSDBA(connect / as sysdba)权限的用户登陆。需要在本地安装或者通过telnet登陆到服务器。 select instance_name,host_name,version,startup_time from v$instance;检查数据文件路径及磁盘空间&…

CNAS中兴新支点——软件测试报告模板分享

第三方检测机构推荐 欢迎私信或评论咨询&#xff0c;也可看本人收藏夹获取详细咨询信息。下面开始进入正文 1、测试背景&#xff1a; 本次测试的软件是一款名为“XXX”的应用程序&#xff0c;主要用于解决某种需求。本次测试目的是发现软件中的问题和缺陷&#xff0c;并进行优…

【VMware】Windows安装MySQL及网络配置---图文并茂详细介绍

一 安装MySQL ① 连接虚拟机传输MySQL压缩包 先查看虚拟机中的地址 命令&#xff1a; ipconfig 主机连接 在主机连接虚拟机后&#xff0c;将mysql压缩包和Navicat安装包复制到虚拟机下即可 ②解压MySQL压缩包 ③ my文件拷贝mysql安装根目录下 如下图的第一步&#xff0c;…

2024年山东省中职“网络安全”试题——B-3:Web安全之综合渗透测试

B-3&#xff1a;Web安全之综合渗透测试 服务器场景名称&#xff1a;Server2010&#xff08;关闭链接&#xff09; 服务器场景操作系统&#xff1a;"需要环境有问题加q" 使用渗透机场景Kali中的工具扫描服务器&#xff0c;通过扫描服务器得到web端口&#xff0c;登陆…

计算机专业个人简历范文(8篇)

HR浏览一份简历也就25秒左右&#xff0c;如果你连「好简历」都没有&#xff0c;怎么能找到好工作呢&#xff1f; 如果你不懂得如何在简历上展示自己&#xff0c;或者觉得怎么改简历都不出彩&#xff0c;那请你一定仔细读完。 互联网运营个人简历范文> 男 22 本科 AI简历…

【Pytorch】Pytorch或者CUDA版本不符合问题解决与分析

NVIDIA CUDA Toolkit Release Notes Package installation issues INSTALL PYTORCH 先声毒人&#xff1a;最好资料就是上面三份资料&#xff0c;可以通过官网明确的获取一手信息&#xff0c;你所遇到的99%的问题都可以找到&#xff0c;明确的解决方案&#xff0c;建议最好看…

linux sh 脚本文件换行错误

windows 写好的脚本到服务运行不起来&#xff0c;显示换行问题 因为 windwos 的换行和 linux 的换行风格不同 解决办法&#xff1a;在使用的文本编辑器中&#xff0c;修改格式为 unix 格式 以 notepad 为例&#xff0c;在编辑 -> 文档格式转换中设置格式为 Unix

fmincon函数的决策变量可以是二维矩阵,但不建议是高维矩阵

1&#xff09;二维矩阵代码 clear all clc% 定义目标函数 fun (x) sum(sum(x.^2));% 初始矩阵 x0 2 rand(2, 2);% 定义空的线性不等式约束 A []; b [];% 定义空的线性等式约束 Aeq []; beq [];% 定义变量的上下界 lb ones(2,2); ub [];% 使用 fmincon 求解 options …

Rancher 单节点 docker 部署备份与恢复

Rancher 单节点 docker 部署备份与恢复 1. 备份集群 获取 rancher server 容器名&#xff0c;本例为 angry_aryabhata docker ps | grep rancher/rancher6a27b8634c80 rancher/rancher:v2.5.14 xxx angry_aryabhata停止容器 docker stop angry_aryabhata创建备…

基于Spring Boot的美妆分享系统:打造个性化推荐、互动社区与智能决策

基于Spring Boot的美妆分享系统&#xff1a;打造个性化推荐、互动社区与智能决策 1. 项目介绍2. 管理员功能2.1 美妆管理2.2 页面管理2.3 链接管理2.4 评论管理2.5 用户管理2.6 公告管理 3. 用户功能3.1 登录注册3.2 分享商品3.3 问答3.4 我的分享3.5 我的收藏夹 4. 创新点4.1 …

国标GB28181对接的时候如何配置服务端口和本地端口

目 录 一、国标GB28181对接需要配置的端口等参数 二、GB28181服务器端口的配置&#xff1a;SIP服务器端口 三、GB28181设备测端口的配置&#xff1a;本地SIP端口 &#xff08;一&#xff09;本地SIP端口配置的意义 &#xff08;二&#xf…

57.网游逆向分析与插件开发-游戏增加自动化助手接口-接管游戏的自动药水设定功能

内容来源于&#xff1a;易道云信息技术研究院VIP课 码云地址&#xff08;master分支&#xff09;&#xff1a;https://gitee.com/dye_your_fingers/sro_-ex.git 码云版本号&#xff1a;51307d6bf69f2f3c645c70d09f841f5e32da79b9 代码下载地址&#xff0c;在 SRO_EX 目录下&…

全志R128使用SPI驱动ST7789V1.47寸LCD

R128 平台提供了 SPI DBI 的 SPI TFT 接口&#xff0c;具有如下特点&#xff1a; Supports DBI Type C 3 Line/4 Line Interface ModeSupports 2 Data Lane Interface ModeSupports data source from CPU or DMASupports RGB111/444/565/666/888 video formatMaximum resoluti…