这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战
1. 简介
压缩列表 ziplist 底层是一个字节数组,是 Redis 为了节约内存而设计的数据结构,可以存储多个元素,每个元素可以是字节数组或整数
压缩列表应用于 Redis 的有序集合、散列、列表:
- 当有序集合的元素较少且都是短字符串时,使用的数据结构为压缩列表
- 列表使用的数据结构是快速列表 quicklist ,由双向链表和压缩列表组合而成
- 散列的底层结构也包含压缩列表
2. 压缩链表的存储结构
2.1 压缩列表
压缩列表的属性:
- zlbytes:表示压缩列表的字节长度,占4字节,因此压缩列表最多有2^32-1个字节
- zltail:表示尾元素相对于压缩列表首地址的偏移量,占4字节
- zllen:表示元素的个数,占2字节,因此压缩列表存储元素个数不超过2^16-1,必须遍历整个压缩列表才能获取元素个数
- entryX:表示压缩列表存储的元素,可以是字节数组或整数
- zlend:表示压缩列表的尾部,占1字节,恒为0xFF
Redis 中压缩列表属性相关宏定义:
// ziplist.c
// zl 指向 zlbytes
#define ZIPLIST_BYTES(zl) (*((uint32_t*)(zl)))
// zl+4 指向 zltail
#define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t))))
// zl+8 指向 zllen
#define ZIPLIST_LENGTH(zl) (*((uint16_t*)((zl)+sizeof(uint32_t)*2)))
// 压缩列表头部大小10字节(zlbytes+zltail+zllen)
#define ZIPLIST_HEADER_SIZE (sizeof(uint32_t)*2+sizeof(uint16_t))
// 压缩列表尾部大小1字节
#define ZIPLIST_END_SIZE (sizeof(uint8_t))
// 压缩列表的头元素
#define ZIPLIST_ENTRY_HEAD(zl) ((zl)+ZIPLIST_HEADER_SIZE)
// 压缩列表的尾元素
#define ZIPLIST_ENTRY_TAIL(zl) ((zl)+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl)))
// 压缩列表元素的尾部
#define ZIPLIST_ENTRY_END(zl) ((zl)+intrev32ifbe(ZIPLIST_BYTES(zl))-1)
2.2 压缩列表元素的编码结构
元素编码结构的属性:
- previous_entry_length:表示前一个元素的字节长度,占1或5个字节
- 前一个元素的字节长度小于254时,占1个字节
- 前一个元素的字节长度大于等于254时,占5个字节,此时第1个字节固定为0xFE,后4个字节表示长度
- 假设存在某个元素的首地址为 p ,那么 p-previous_entry_length 表示前一个元素的首地址,由此可以实现压缩列表从尾向头遍历
- encoding:表示元素的编码,即 content 的数据类型(整数或字节数组)
- content:存储元素的数据内容
为了节约内存,encoding 属性的长度是可变的:
| encoding 编码 | encoding 长度 | content 类型 |
|---|---|---|
| 00 bbbbbb(6比特表示 content 长度) | 1字节 | 最大长度为63的字节数组 |
| 01 bbbbbb cccccccc(14比特表示 content 长度) | 2字节 | 最大长度为2^14-1的字节数组 |
| 10------ aaaaaaaa bbbbbbbb cccccccc dddddddd(32比特表示 content 长度) | 5字节 | 最大长度为2^32-1的字节数组 |
| 11000000 | 1字节 | int16 整数 |
| 11010000 | 1字节 | int32 整数 |
| 11100000 | 1字节 | int64 整数 |
| 11110000 | 1字节 | 24位整数 |
| 11111110 | 1字节 | 8位整数 |
| 1111 xxxx | 1字节 | 没有 content 属性,xxxx 表示区间 (0,12] 的整数 |
- 根据 encoding 属性的第1个字节的前2位,可以判断 content 属性存储的是整数或字节数组(及其最大长度)
- 当 content 存储的是字节数组时,encoding 属性第1个字节的前2位后的字节标识了字节数组的实际长度
- 当 content 存储的是整数时,encoding 属性第1个字节的3、4位标识了整数的类型。当 encoding 属性标识元素存储的是区间 (0,12] 的整数时,整数内容直接存储在 encoding 属性中,此时没有 content 属性
Redis 中定义了以下常量标识 encoding 属性的编码类型:
-
// ziplist.c #define ZIP_STR_06B (0 << 6) // 00000000 #define ZIP_STR_14B (1 << 6) // 01000000 #define ZIP_STR_32B (2 << 6) // 10000000 #define ZIP_INT_16B (0xc0 | 0<<4) // 11000000 = 11000000 | 00000000 #define ZIP_INT_32B (0xc0 | 1<<4) // 11010000 = 11000000 | 00010000 #define ZIP_INT_64B (0xc0 | 2<<4) // 11100000 = 11000000 | 00100000 #define ZIP_INT_24B (0xc0 | 3<<4) // 11110000 = 11000000 | 00110000 #define ZIP_INT_8B 0xfe // 11111110
3. 结构体
3.1 元素编码缓存
压缩列表元素编码结构复杂,当解码后获取到元素的前一个元素的长度、元素存储的数据类型、元素的数据内容时,可以将这些属性缓存到结构体 zlentry
-
// ziplist.c typedef struct zlentry { unsigned int prevrawlensize; unsigned int prevrawlen; unsigned int lensize; unsigned int len; unsigned int headersize; unsigned char encoding; unsigned char *p; } zlentry; -
prevrawlensize:previous_entry_length 属性的字节长度
-
prevrawlen:前一个元素的字节长度
-
lensize:encoding 属性的字节长度
-
len:元素数据内容的字节长度
-
headersize:元素头部的字节长度(prevrawlensize+lensize)
-
encoding:元素数据内容的类型
-
p:元素的首地址
3.2 元素解码
-
// ziplist.c void zipEntry(unsigned char *p, zlentry *e) { // 解析 prevrawlensize、prevrawlen ZIP_DECODE_PREVLEN(p, e->prevrawlensize, e->prevrawlen); // 解析 encoding、lensize、len ZIP_DECODE_LENGTH(p + e->prevrawlensize, e->encoding, e->lensize, e->len); // 计算 headersize e->headersize = e->prevrawlensize + e->lensize; // 设置 p e->p = p; } -
// ziplist.c // ptr 指向元素首地址,即 previous_entry_length 属性的首地址 #define ZIP_DECODE_PREVLEN(ptr, prevlensize, prevlen) do { // 解析 prevlensize ZIP_DECODE_PREVLENSIZE(ptr, prevlensize); if ((prevlensize) == 1) { // 如果 previous_entry_length 属性占1个字节 // 则根据该字节解析出 prevlen (prevlen) = (ptr)[0]; } else if ((prevlensize) == 5) { // 如果 previous_entry_length 属性占5个字节 // 则根据第2~5个字节解析出 prevlen assert(sizeof((prevlen)) == 4); memcpy(&(prevlen), ((char*)(ptr)) + 1, 4); memrev32ifbe(&prevlen); } } while(0); #define ZIP_BIG_PREVLEN 254 #define ZIP_DECODE_PREVLENSIZE(ptr, prevlensize) do { if ((ptr)[0] < ZIP_BIG_PREVLEN) { // 如果前一个元素字节长度小于254 // 则 previous_entry_length 属性占1个字节,prevlensize=1 (prevlensize) = 1; } else { // 如果前一个元素字节长度大于等于254 // 则 previous_entry_length 属性占5个字节,prevlensize=5 (prevlensize) = 5; } } while(0); -
// ziplist.c #define ZIP_STR_MASK 0xc0 // 11000000 // ptr 指向元素 encoding 属性的首地址 #define ZIP_DECODE_LENGTH(ptr, encoding, lensize, len) do { // 解析 encoding ZIP_ENTRY_ENCODING((ptr), (encoding)); if ((encoding) < ZIP_STR_MASK) { // 元素为字节数组时 // 根据第一个字节前两位区分出编码类型(最大长度) // 然后进一步解析出 lensize、len if ((encoding) == ZIP_STR_06B) { (lensize) = 1; (len) = (ptr)[0] & 0x3f; } else if ((encoding) == ZIP_STR_14B) { (lensize) = 2; (len) = (((ptr)[0] & 0x3f) << 8) | (ptr)[1]; } else if ((encoding) == ZIP_STR_32B) { (lensize) = 5; (len) = ((ptr)[1] << 24) | ((ptr)[2] << 16) | ((ptr)[3] << 8) | ((ptr)[4]); } else { panic("Invalid string encoding 0x%02X", (encoding)); } } else { // 元素为整数时,encoding 长度为1字节 (lensize) = 1; // 解析整数长度 len (len) = zipIntSize(encoding); } } while(0); #define ZIP_ENTRY_ENCODING(ptr, encoding) do { (encoding) = (ptr[0]); // 处理 encoding 属性第1个字节: // 1. ptr[0] < ZIP_STR_MASK,说明为字节数组,根据前2个比特可知字节数组编码类型 // 2. ptr[0] >= ZIP_STR_MASK ,说明为整数,不处理 if ((encoding) < ZIP_STR_MASK) (encoding) &= ZIP_STR_MASK; } while(0); unsigned int zipIntSize(unsigned char encoding) { // 对比 encoding 属性的编码类型,解析出 len switch(encoding) { case ZIP_INT_8B: return 1; case ZIP_INT_16B: return 2; case ZIP_INT_24B: return 3; case ZIP_INT_32B: return 4; case ZIP_INT_64B: return 8; } // 区间 (0,12] 的整数,content 属性不存在,len=0 if (encoding >= ZIP_INT_IMM_MIN && encoding <= ZIP_INT_IMM_MAX) return 0; panic("Invalid integer encoding 0x%02X", encoding); return 0; }
4. 基本操作
4.1 创建压缩链表
// ziplist.c
unsigned char *ziplistNew(void) {
// 初始内存空间大小 = 压缩列表头部+尾部 = zlbytes+zltail+zllen+zlend = 4+4+2+1 = 11
unsigned int bytes = ZIPLIST_HEADER_SIZE+1;
// 分配内存
unsigned char *zl = zmalloc(bytes);
// 初始压缩链表相关属性
ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);
ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);
ZIPLIST_LENGTH(zl) = 0;
// 设置尾部标识0xFF
zl[bytes-1] = ZIP_END;
return zl;
}
4.2 插入元素
压缩列表插入元素步骤:
- 元素编码
- 重新分配空间
- 数据复制
4.2.1 编码
编码即解析元素的 previous_entry_length、encoding、content 属性
4.2.1.1 插入元素位置的情况
- p0:压缩列表没有任何元素时,不存在前一个元素,即前一个元素的字节长度为0
- p1:在压缩列表已存在元素的中间插入时,需要获取 entryX 元素的字节长度,而 entryX+1 元素的 previous_entry_length 属性存储了 entryX 元素的字节长度,可以直接获取
- p2:在压缩列表的尾部元素后插入时,需要获取 entryN 元素的字节长度,此时需要解析出 entryN 元素的 prevrawlensize、lensize、len 后相加得到长度
4.2.1.2 解析元素属性
// ziplist.c
// zl:指向压缩列表首地址
// p:指向元素插入位置
// s:指向插入的数据内容
// slen:表示数据内容的字节长度
unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {
size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), reqlen;
unsigned int prevlensize, prevlen = 0;
size_t offset;
int nextdiff = 0;
unsigned char encoding = 0;
long long value = 123456789;
zlentry tail;
if (p[0] != ZIP_END) {
// 插入位置是压缩列表已存在元素的中间
// 调用 ZIP_DECODE_PREVLEN() 解析出 prevlen
ZIP_DECODE_PREVLEN(p, prevlensize, prevlen);
} else {
// 插入位置是压缩列表的尾部
unsigned char *ptail = ZIPLIST_ENTRY_TAIL(zl);
// 尾元素存在时,调用 zipRawEntryLength() 解析出 prevlen
// 尾元素不存在时,没有前一个元素,prevlen=0
if (ptail[0] != ZIP_END) {
prevlen = zipRawEntryLength(ptail);
}
}
// 尝试按照整数解析数据内容 s
// 成功时,根据 encoding 进一步解析整数 s 的字节长度
// 失败时,s 为字节数组,字节长度为 slen
if (zipTryEncoding(s,slen,&value,&encoding)) {
reqlen = zipIntSize(encoding);
} else {
reqlen = slen;
}
// 获取 previous_entry_length、encoding 属性的字节长度,并累加到 reqlen
reqlen += zipStorePrevEntryLength(NULL,prevlen);
reqlen += zipStoreEntryEncoding(NULL,encoding,slen);
// 此时已解析出插入元素的字节长度
// 以及三个属性的字节长度和值
// ......
}
4.2.1.3 按照整数解析
// ziplist.c
int zipTryEncoding(unsigned char *entry, unsigned int entrylen, long long *v, unsigned char *encoding) {
long long value;
// 如果数据内容字节长度等于0或大于等于32,则返回0
if (entrylen >= 32 || entrylen == 0) return 0;
if (string2ll((char*)entry,entrylen,&value)) {
// 数据内容解析为 long long 类型的整数 value
// 进一步解析出 encoding 属性
if (value >= 0 && value <= 12) {
*encoding = ZIP_INT_IMM_MIN+value;
} else if (value >= INT8_MIN && value <= INT8_MAX) {
*encoding = ZIP_INT_8B;
} else if (value >= INT16_MIN && value <= INT16_MAX) {
*encoding = ZIP_INT_16B;
} else if (value >= INT24_MIN && value <= INT24_MAX) {
*encoding = ZIP_INT_24B;
} else if (value >= INT32_MIN && value <= INT32_MAX) {
*encoding = ZIP_INT_32B;
} else {
*encoding = ZIP_INT_64B;
}
// 将 value 的值传递给调用方
*v = value;
return 1;
}
return 0;
}
4.2.1.4 获取 previous_entry_length 属性的字节长度
// ziplist.c
unsigned int zipStorePrevEntryLength(unsigned char *p, unsigned int len) {
if (p == NULL) {
// p == NULL 时,直接返回 previous_entry_length 属性占用的字节长度
// len < 254 时,previous_entry_length 属性占用1个字节
// len >= 254 时,previous_entry_length 属性占用5个字节
return (len < ZIP_BIG_PREVLEN) ? 1 : sizeof(len)+1;
} else {
// p != NULL 时,将 len 存储到 previous_entry_length 属性中
if (len < ZIP_BIG_PREVLEN) {
// previous_entry_length 属性占用1个字节时
// 第1个字节存储 len
p[0] = len;
return 1;
} else {
return zipStorePrevEntryLengthLarge(p,len);
}
}
}
int zipStorePrevEntryLengthLarge(unsigned char *p, unsigned int len) {
if (p != NULL) {
// previous_entry_length 属性占用5个字节时
// 第1个字节固定为0xFE
// 后4个字节存储长度 len
p[0] = ZIP_BIG_PREVLEN;
memcpy(p+1,&len,sizeof(len));
memrev32ifbe(p+1);
}
return 1+sizeof(len);
}
4.2.1.5 获取 encoding 属性的字节长度
// ziplist.c
unsigned int zipStoreEntryEncoding(unsigned char *p, unsigned char encoding, unsigned int rawlen) {
unsigned char len = 1, buf[5];
if (ZIP_IS_STR(encoding)) {
// 数据类型为字节数组
// p=NULL 时,直接返回 encoding 属性占用的字节长度
// p!=NULL 时,将字节数组的类型和长度存储到 encoding 属性中
if (rawlen <= 0x3f) {
// 字节数组长度小于等于63,encoding 属性占用1个字节
if (!p) return len;
buf[0] = ZIP_STR_06B | rawlen;
} else if (rawlen <= 0x3fff) {
// 字节数组长度区间(63,2^14-1],encoding 属性占用2个字节
len += 1;
if (!p) return len;
buf[0] = ZIP_STR_14B | ((rawlen >> 8) & 0x3f);
buf[1] = rawlen & 0xff;
} else {
// 字节数组长度区间(2^14-1,2^32-1],encoding 属性占用5个字节
len += 4;
if (!p) return len;
buf[0] = ZIP_STR_32B;
buf[1] = (rawlen >> 24) & 0xff;
buf[2] = (rawlen >> 16) & 0xff;
buf[3] = (rawlen >> 8) & 0xff;
buf[4] = rawlen & 0xff;
}
} else {
// 数据类型为整数
// p=NULL 时,则直接返回 encoding 属性所需的字节长度
// p!=NULL 时,将整数的类型存储到 encoding 属性中
if (!p) return len;
buf[0] = encoding;
}
memcpy(p,buf,len);
return len;
}
4.2.2 重新分配空间
插入新元素时,压缩列表的空间会变大,因此需要重新分配空间
新空间大小=旧空间大小+新元素空间大小,该等式不完全成立。假设插入元素前,entryX 元素长度为128个字节,entryX+1 元素的 previous_entry_length 属性占1个字节。添加长度为1024个字节的 entryNew 元素时,entryX+1 元素的 previous_entry_length 属性需要占5个字节,即压缩链表的长度不仅增加了1024个字节,还要加上 entryX+1 元素扩展的4个字节。 entryX+1 元素长度可能增加4个字节,减少4个字节或不变
// ziplist.c
unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {
// ......
int forcelarge = 0;
// nextdiff 表示插入新元素时,其下个元素字节长度的变化,可能为0、-4、+4
// 当插入位置在压缩列表尾部时,不存在下个元素,nextdiff=0
// 否则进一步计算 nextdiff
nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0;
if (nextdiff == -4 && reqlen < 4) {
// nextdiff = -4 且 reqlen < 4 时,压缩链表插入新元素导致空间变小了
// 重新分配空间可能导致数据丢失(此时还没数据复制)
// 为避免这种情况,重新赋值 nextdiff=0 ,同时进行标记 forcelarge=1
// 上述情况一般不会发生,只有在连锁更新后可能存在
nextdiff = 0;
forcelarge = 1;
}
// 重新分配空间,可能导致插入位置 p 失效
// 因此记录指针 p 相对压缩列表首地址 zl 的偏移量 offset
offset = p-zl;
// 重新分配空间
zl = ziplistResize(zl,curlen+reqlen+nextdiff);
// 重新计算插入位置 p
p = zl+offset;
// ......
}
int zipPrevLenByteDiff(unsigned char *p, unsigned int len) {
unsigned int prevlensize;
// 计算 p 指向元素的 previous_entry_length 属性的字节长度 prevlensize
ZIP_DECODE_PREVLENSIZE(p, prevlensize);
// 计算字节长度 len 对应的 previous_entry_length 属性的字节长度
// 然后与 prevlensize 相减
return zipStorePrevEntryLength(NULL, len) - prevlensize;
}
4.2.3 数据复制
重新分配内存后,需要将插入位置 p 后的元素移动到指定位置,腾出待插入元素的内存空间,然后再插入元素
// ziplist.c
unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {
// ......
if (p[0] != ZIP_END) {
// 如果不是尾部插入,则需要移动插入位置 p 后的元素
// 复制 p 后的所有元素到新位置
// 数据块长度为 p 后所有元素的总字节长度加上 nextdiff
memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff);
// 更新待插入元素的后一个元素的 previous_entry_length 属性
if (forcelarge)
zipStorePrevEntryLengthLarge(p+reqlen,reqlen);
else
zipStorePrevEntryLength(p+reqlen,reqlen);
// 更新 zltail
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+reqlen);
// 将待插入元素后一个元素的解码为 tail
zipEntry(p+reqlen, &tail);
if (p[reqlen+tail.headersize+tail.len] != ZIP_END) {
// 如果待插入元素的后一个元素不是尾元素,则 zltail 还需要偏移 nextdiff
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+nextdiff);
}
} else {
// 如果尾部插入,则插入位置 p (待插入元素首地址) 到压缩列表首地址的偏移量为 zltail
ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(p-zl);
}
if (nextdiff != 0) {
// 如果 nextdiff不为0,则此时压缩列表可能需要连续更新
// 从而导致压缩列表首地址和插入位置 p 失效,所以可能需要重新赋值
offset = p-zl;
zl = __ziplistCascadeUpdate(zl,p+reqlen);
p = zl+offset;
}
// 将待插入元素的 previous_entry_length、encoding、content 编码后存储在插入位置 p
p += zipStorePrevEntryLength(p,prevlen);
p += zipStoreEntryEncoding(p,encoding,slen);
if (ZIP_IS_STR(encoding)) {
memcpy(p,s,slen);
} else {
zipSaveInteger(p,value,encoding);
}
// zllen 加1
ZIPLIST_INCR_LENGTH(zl,1);
return zl;
}
4.3 删除元素
压缩列表删除元素的步骤:
- 计算待删除元素的总长度
- 数据复制
- 重新分配内存
4.3.1 计算待删除元素的总长度
// ziplist.c
unsigned char *__ziplistDelete(unsigned char *zl, unsigned char *p, unsigned int num) {
unsigned int i, totlen, deleted = 0;
size_t offset;
int nextdiff = 0;
zlentry first, tail;
// 解码第1个待删除元素 first
zipEntry(p, &first);
// 遍历所有待删除元素,指针 p 移动到最后一个待删除元素的尾地址
// 同时对待删除元素计数 deleted
for (i = 0; p[0] != ZIP_END && i < num; i++) {
p += zipRawEntryLength(p);
deleted++;
}
// 计算待删除元素的总长度
totlen = p-first.p;
// ......
}
4.3.2 数据复制
新空间大小=旧空间大小-待删除元素的总长度,该等式不完全成立。假设删除元素 entryX+1 到元素 entryN-1 之间的元素,元素 entryN-1 长度为12个字节,所以元素 entryN 的 previous_entry_length 属性占用1个字节。删除元素后,长度为512个字节的元素 entryX 成为元素 entryN 的前一个元素,此时元素 entryN 的 previous_entry_length 属性需要占用5个字节,即删除元素之后的压缩列表的总长度还与元素 entryN 长度有关
// ziplist.c
unsigned char *__ziplistDelete(unsigned char *zl, unsigned char *p, unsigned int num) {
// ......
if (totlen > 0) {
if (p[0] != ZIP_END) {
// 如果待删除元素删除后,p 指向元素存在
// 计算 p 指向元素字节长度的变化 nextdiff
nextdiff = zipPrevLenByteDiff(p,first.prevrawlen);
// 根据 nextdiff 移动 p
p -= nextdiff;
// 更新 p 指向元素的 previous_entry_length 属性存储的前一个元素的字节长度
zipStorePrevEntryLength(p,first.prevrawlen);
// 根据 totlen 更新 zltail
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))-totlen);
// 解码 p 指向元素 tail
zipEntry(p, &tail);
if (p[tail.headersize+tail.len] != ZIP_END) {
// 元素 tail 后还存在元素时,更新 zltail
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+nextdiff);
}
// 复制 p 后的所有元素到新位置
// 数据块长度为 p 后所有元素的总字节长度
memmove(first.p,p,
intrev32ifbe(ZIPLIST_BYTES(zl))-(p-zl)-1);
} else {
// 如果待删除元素删除后,p 指向元素不存在
// 直接计算出 zltail ,无需数据复制
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe((first.p-zl)-first.prevrawlen);
}
// ......
}
// ......
}
4.3.3 重新分配内存
// ziplist.c
unsigned char *__ziplistDelete(unsigned char *zl, unsigned char *p, unsigned int num) {
// ......
if (totlen > 0) {
// ......
// 重新分配内存时,导致压缩列表首地址和指针 p 失效
// 需要重新赋值
offset = first.p-zl;
zl = ziplistResize(zl, intrev32ifbe(ZIPLIST_BYTES(zl))-totlen+nextdiff);
// 更新 zllen
ZIPLIST_INCR_LENGTH(zl,-deleted);
p = zl+offset;
if (nextdiff != 0)
// nextdiff!=0 时,p 指向元素的字节长度发生变化
// 可能导致后面的元素需要连锁更新
zl = __ziplistCascadeUpdate(zl,p);
}
return zl;
}
4.4 遍历压缩列表
4.4.1 前向遍历
// ziplist.c
unsigned char *ziplistNext(unsigned char *zl, unsigned char *p) {
((void) zl);
if (p[0] == ZIP_END) {
// p 指向 zlend 时,没有元素可遍历了
return NULL;
}
// 获取 p 指向元素的字节长度,移动该长度后指向下一个元素首地址
p += zipRawEntryLength(p);
if (p[0] == ZIP_END) {
return NULL;
}
return p;
}
4.4.2 后向遍历
// ziplist.c
unsigned char *ziplistPrev(unsigned char *zl, unsigned char *p) {
unsigned int prevlensize, prevlen = 0;
if (p[0] == ZIP_END) {
// p 指向 zlend 时,将 p 重新指向尾元素
// 尾元素存在时返回
// 尾元素不存在时,表示没有元素可遍历了
p = ZIPLIST_ENTRY_TAIL(zl);
return (p[0] == ZIP_END) ? NULL : p;
} else if (p == ZIPLIST_ENTRY_HEAD(zl)) {
// p 指向头元素时,没有元素可遍历了
return NULL;
} else {
// 根据 p 指向元素的 previous_entry_length 属性存储的前一个元素的字节长度
// 将 p 重新指向前一个元素的首地址后返回
ZIP_DECODE_PREVLEN(p, prevlensize, prevlen);
assert(prevlen > 0);
return p-prevlen;
}
}
4.5 连锁更新
在插入或删除元素时,除了操作位置的后一个元素的字节长度可能发生变化,还可能导致后续其他元素的字节长度也发生变化
- 压缩列表 zl1 在位置 p1 删除元素 extryX 时,元素 entryX+1 的前一个元素变成元素 entryX-1 ,前一个元素的长度从128字节变成512字节,元素 entryX+1 的长度需要从253字节扩展到257字节(前一个元素字节长度大于等于254字节时,previous_entry_length 属性字节长度为5字节),导致元素 entryX+2 也需要从253字节扩展到257字节,依次类推,发生连锁更新
- 压缩列表 zl2 在位置 p2 插入元素 extryY 时,与上面删除类似,前一个元素的字节长度变大,导致后面元素的字节长度也变大,依次类推,发生连锁更新
- 当插入或删除元素,前一个元素的字节长度变小,导致后面元素的字节长度可以变小时,Redis 实际未做处理
// ziplist.c
unsigned char *__ziplistCascadeUpdate(unsigned char *zl, unsigned char *p) {
size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), rawlen, rawlensize;
size_t offset, noffset, extra;
unsigned char *np;
zlentry cur, next;
// p 指向元素存在时,执行连锁更新逻辑
while (p[0] != ZIP_END) {
// 解码 p 指向元素 cur
zipEntry(p, &cur);
// 获取元素 cur 的字节长度 rawlen
rawlen = cur.headersize + cur.len;
// 获取元素 cur 的 previous_entry_length 属性的字节长度 rawlensize
rawlensize = zipStorePrevEntryLength(NULL,rawlen);
// 如果元素 cur 的下一个元素不存在,则无需执行连锁更新逻辑
if (p[rawlen] == ZIP_END) break;
// p+rawlen 指向下一个元素,解码元素 next
zipEntry(p+rawlen, &next);
// 如果元素 next 的 previous_entry_length 属性存储的前一个元素的字节长度没有发生变化
// 则无需执行连锁更新逻辑
if (next.prevrawlen == rawlen) break;
if (next.prevrawlensize < rawlensize) {
// 如果元素 next 的 previous_entry_length 属性的字节长度变大了
// 重新分配内存时,导致压缩列表首地址和指针 p 失效
// 需要重新赋值
offset = p-zl;
// 计算元素 next 的 previous_entry_length 属性需要扩展的字节长度 extra
extra = rawlensize-next.prevrawlensize;
zl = ziplistResize(zl,curlen+extra);
p = zl+offset;
// np 指向元素 next 的首地址
np = p+rawlen;
// 元素 next 的首地址相对于压缩列表首地址的偏移量 noffset
noffset = np-zl;
if ((zl+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))) != np) {
// 如果元素 next 不是压缩列表的尾元素
// 则需要更新 zltail
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+extra);
}
// 复制元素 next 的 encoding 属性首地址后的数据到新地址
memmove(np+rawlensize,
np+next.prevrawlensize,
curlen-noffset-next.prevrawlensize-1);
// 存储元素 cur 的字节长度 rawlen 到元素 next 的 previous_entry_length 属性中
zipStorePrevEntryLength(np,rawlen);
// 移动指针 p 指向下一个需要执行连锁更新逻辑的元素
p += rawlen;
// 更新压缩列表字节长度 curlen
curlen += extra;
} else {
if (next.prevrawlensize > rawlensize) {
// 如果元素 next 的 previous_entry_length 属性的字节长度可以变小
// 则实际不执行执行连锁更新逻辑
// 只更新元素 next 的 previous_entry_length 属性存储的值(元素 cur 的字节长度)
zipStorePrevEntryLengthLarge(p+rawlen,rawlen);
} else {
// 如果元素 next 的 previous_entry_length 属性的字节长度不变
// 则不执行执行连锁更新逻辑
// 只更新元素 next 的 previous_entry_length 属性存储的值(元素 cur 的字节长度)
zipStorePrevEntryLength(p+rawlen,rawlen);
}
break;
}
}
return zl;
}
学自《Redis 5设计与源码分析》