由于篇幅过长,ziplist的源码分析分为两篇,本篇是第一篇,涉及到ziplist的数据结构、初始化创建、以及ziplist的特殊编码部分的源码分析。
第二篇博客地址:Redis 5.0数据结构之压缩列表ziplist源码详解(二)
概述
Redis基于C语言开发,由于C并未内置链表,因此Redis自己实现了一系列链表,有双端链表linkedlist、压缩列表ziplist、快速链表quicklist等,本篇将从源码角度展现压缩列表ziplist。
笔者相关文章推送:
Redis 5.0数据结构之双端链表linkedlist源码解析
Redis 链表list常用命令总结
Redis 中的list实现
众所周知Redis有五种数据类型,链表是其中的一种,在Redis在3.2版本,修改了list的底层实现,在Redis 3.2之前链表是由双端链表linkedlist和压缩链表ziplist共同实现的,Redis3.2版本之后主要实现优化为了快速链表quicklist。
Redis3.2之前,当同时满足以下两个条件时,列表键的底层实现为ziplist,其余情况均使用linkedlist
- 哈希对象保存的所有键值的字符串长度小于64字节;
- 哈希对象保存的键值对数量小于512个;
可见压缩列表主要是为了节约内存开发。
本篇使用源码版本均为Redis 5.0,其他版本也具有一定的参考价值,主要介绍的压缩列表ziplist源码均来自ziplist.h和ziplist.c两个文件。
压缩列表的构成
笔者认为ziplist是链表中十分特殊的存在。传统意义上或是说更广为人知的list链表实现,都是在节点构造中使用指针,然后直接使用该指针指向当前节点前后节点的地址,来实现链表。但ziplist并不是使用指针直接保存节点内存地址的方式实现,ziplist使用连续内存地址+偏移量的方式实现链表。
理解压缩列表及其源码的重点就在于要理解压缩列表的结构。压缩列表没有数据结构代码定义,压缩列表是一种通过内存特殊编码方式实现的数据结构
压缩列表相关宏定义函数
首先源码中提供了一些宏定义函数,用于获取ziplist相关信息,在下文的源码分析中各方法将频繁出现
/* ziplist结尾特殊标志位 默认0xFF 即255 */
#define ZIP_END 255
/* 返回压缩列表ziplist包含的总字节数 */
#define ZIPLIST_BYTES(zl) (*((uint32_t*)(zl)))
/* 返回ziplist中最后一个节点的偏移量 */
#define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t))))
/* 返回ziplist的总长度
* 该属性值超过UINT16_MAX(65536)时,返回UINT16_MAX,表示真实长度需要遍历得出 */
#define ZIPLIST_LENGTH(zl) (*((uint16_t*)((zl)+sizeof(uint32_t)*2)))
/* 返回ziplist的header长度(header总所需字节数) 总共10字节 header包括
* 一个32位的属性用于表示ziplist的总字节数
* 一个32位的属性用于表示起始地址到尾节点的偏移量
* 一个16位的属性用于表示节点数,即ziplist长度 */
#define ZIPLIST_HEADER_SIZE (sizeof(uint32_t)*2+sizeof(uint16_t))
/* 返回ziplist结尾标志的长度(结尾所需字节数) 总共1字节 */
#define ZIPLIST_END_SIZE (sizeof(uint8_t))
/* 返回ziplist中指向第一个节点的指针 */
#define ZIPLIST_ENTRY_HEAD(zl) ((zl)+ZIPLIST_HEADER_SIZE)
/* 返回ziplist中指向最后一个节点的指针 使用header中的的偏移量获取 */
#define ZIPLIST_ENTRY_TAIL(zl) ((zl)+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl)))
/* 返回指向ziplist结尾标识(0xFF)的指针 */
#define ZIPLIST_ENTRY_END(zl) ((zl)+intrev32ifbe(ZIPLIST_BYTES(zl))-1)
压缩列表创建函数
由于压缩列表基于内存特殊编码实现,源码种并没有数据结构代码定义,但可以通过上文的宏定义函数,结合压缩列表的创建函数来推出压缩列表的大致组成部分。压缩列表创建函数如下:
/* Create a new empty ziplist. */
unsigned char *ziplistNew(void) {
// 为压缩列表申请bytes字节空间
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);
// 设置len属性为0 len表示ziplist中节点数量
ZIPLIST_LENGTH(zl) = 0;
// 设置ziplist结尾标志0xFF
zl[bytes-1] = ZIP_END;
return zl;
}
压缩列表组成部分
由源码可见压缩列表共有如下五个组成部分:
属 性 | 类 型 | 长 度 | 用 途 |
---|---|---|---|
zlbytes | unit32_t | 4字节 | 记录整个压缩列表占用的内存字节数,包括zlbytes的四字节:在对压缩列表进行内存重分配, 或者计算 zlend 的位置时使用。 |
zltail | unit32_t | 4字节 | 记录压缩列表表尾节点距离压缩列表的起始地址有多少字节: 通过这个偏移量,程序无须遍历整个压缩列表就可以确定表尾节点的地址。 |
zlen | unit16_t | 2字节 | 记录了压缩列表包含的节点数量: 当这个属性的值小于 UINT16_MAX (65535)时, 这个属性的值就是压缩列表包含节点的数量; 当这个值等于 UINT16_MAX 时, 节点的真实数量需要遍历整个压缩列表才能计算得出。 |
entryX | 列表节点 | 不定 | 压缩列表包含的各个节点,节点的长度由节点保存的内容决定。 |
zlend | unit8_t | 1字节 | 特殊值 0xFF (十进制 255 ),用于标记压缩列表的末端。 |
压缩列表组成示意图如下:
压缩列表节点的构成
压缩列表中有五个组成部分,其中最终要当属实际存储数据的zlentry节点,接下来将通过源码分析压缩列表节点的组成。
压缩列表节点结构定义
与压缩列表本身不同,节点具有明确的结构定义,压缩列表的节点在ziplist.c中定义如下:
typedef struct zlentry {
// 用于记录内存编码后前一个entry的len占用了多少字节 即prevrawlen占用了多少字节
unsigned int prevrawlensize;
// 用于记录前一个entry占用的长度
// 通过当前entry地址 - prevrawlen可以寻找到上一个entry的首地址
unsigned int prevrawlen;
// 用于记录内存编码后当前entry的len占用了多少字节
unsigned int lensize;
// 用于记录当前entry的长度
unsigned int len;
// 用于记录当前entry的header大小 即lensize + prevrawsize
unsigned int headersize;
// 用于记录当前节点编码格式
unsigned char encoding;
// 指向当前节点的指针
unsigned char *p;
} zlentry;
prevrawlen的编码
zlentry中的prevrawlen长度并不是固定的,在prevrawlen的处理上使用了压缩编码,相关函数如下:
#define ZIP_BIG_PREVLEN 254
/* 对prevrawlen进行编码 或者直接返回编码len长度所需要的字节数 */
unsigned int zipStorePrevEntryLength(unsigned char *p, unsigned int len) {
// 当传入p为NULL时 自然不需要编码 直接返回编码len长度所需要的字节数
if (p == NULL) {
return (len < ZIP_BIG_PREVLEN) ? 1 : sizeof(len)+1;
} else {
// 如果len小于254 则直接使用一个字节保存
if (len < ZIP_BIG_PREVLEN) {
p[0] = len;
return 1;
// len大于等于254时调用函数处理
} else {
return zipStorePrevEntryLengthLarge(p,len);
}
}
}
int zipStorePrevEntryLengthLarge(unsigned char *p, unsigned int len) {
if (p != NULL) {
// 首先首位固定设为254 表示节点长度一定大于等于254字节
p[0] = ZIP_BIG_PREVLEN;
// 随后开辟所需内存空间
memcpy(p+1,&len,sizeof(len));
// 大小端转换
memrev32ifbe(p+1);
}
// p为NULL时直接返回编码len所需字节数
return 1+sizeof(len);
}
从以上两个函数中可以发现,prevrawlen的编码有两种可能,即属性长度只可能是1字节或5字节
- 当前一个节点长度小于254字节时,直接将前一节点存储在当前字节中
- 当前一个节点长度大于等于254字节时,将prevrawlen的第一个字节设为254,后四个字节用于保存前一节点长度(因为unsigned int固定4字节)。
len与encoding编码
源码阅读到现在的部分,仍有一个重要的问题没用理清,就是zlentry能存放什么数据,这里先给出结论,zlentry被设置允许存放字符串和整数类型数据,由于压缩列表的主要目的是尽量节省空间,所以字符串和整数类型的数据有也自然有他的压缩(编码)方式。
字符串编码
#define ZIP_STR_MASK 0xc0
#define ZIP_INT_MASK 0x30
#define ZIP_IS_STR(enc) (((enc) & ZIP_STR_MASK) < ZIP_STR_MASK)
#define ZIP_STR_06B (0 << 6)
#define ZIP_STR_14B (1 << 6)
#define ZIP_STR_32B (2 << 6)
/* 与prevrawlen编码相同 p不为NULL时写入 为NULL时直接返回长度 */
unsigned int zipStoreEntryEncoding(unsigned char *p, unsigned char encoding, unsigned int rawlen) {
unsigned char len = 1, buf[5];
// 判断存放的是否是字符串
if (ZIP_IS_STR(encoding)) {
// 长度 <= 0x3f 即63
if (rawlen <= 0x3f) {
// 此处len不变 即只占位1字节
if (!p) return len;
// 该字节高位存放标识ZIP_STR_06B 0 << 6 即该字节高二位是00
// 后6位实际存放长度 故表示能够存放63个字节
buf[0] = ZIP_STR_06B | rawlen;
// 长度 <= 0x3fff 即16383
} else if (rawlen <= 0x3fff) {
// len扩大 占位2字节
len += 1;
if (!p) return len;
// 首位字节高二位为ZIP_STR_14B 1 << 6 即高二位01
// 其余14位实际存放长度 故表示能存放16383个字节
buf[0] = ZIP_STR_14B | ((rawlen >> 8) & 0x3f);
buf[1] = rawlen & 0xff;
// 其余情况 实际上是 长度小于4294967295
} else {
// len扩大 占位5字节
len += 4;
if (!p) return len;
// 首位字节为ZIP_STR_32B 2 << 6 高二位固定10 该字节其余位不使用
// 剩余四个字节用于存放长度 故表示能存放4294967295个字节
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 {
// 不是字符串 那么编码编码为整型 长度固定位1 见整数编码
if (!p) return len;
buf[0] = encoding;
}
// p中存放
memcpy(p,buf,len);
return len;
}
从字符串编码的函数中可以明显的发现,在存放字符串(字节数组)时,有三种可能的长度,下附字节数组编码表格,其中x表示实际存放长度的位,_表示弃用不适用的位。
编码 | 编码长度 | 实际存储的数据 |
---|---|---|
00xxxxxx | 1字节 | 长度小于等于63字节的字节数组 |
01xxxxxx xxxxxxxx | 2字节 | 长度小于等于16383字节的字节数组 |
10_ _ _ _ _ _ xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx | 5字节 | 长度小于4294 967 295字节的字节数组 |
整数编码
// 观察可见整数编码的高二位固定11 0xc0 = 1100 0000
#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
int zipTryEncoding(unsigned char *entry, unsigned int entrylen, long long *v, unsigned char *encoding) {
// 存放实际数据
long long value;
if (entrylen >= 32 || entrylen == 0) return 0;
if (string2ll((char*)entry,entrylen,&value)) {
/* Great, the string can be encoded. Check what's the smallest
* of our encoding types that can hold this value. */
// 0~12的整数值特殊 直接保存在encoding字段中
if (value >= 0 && value <= 12) {
*encoding = ZIP_INT_IMM_MIN+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;
}
从整型编码函数中可见,在存放整数时,有六种可能的长度,附整数编码表格如下:
编码 | 编码长度 | 实际存储的数据 |
---|---|---|
11000000 | 1字节 | int16_t类型的整数 |
11010000 | 1字节 | int32_t类型的整数 |
11100000 | 1字节 | int64_t类型的整数 |
11110000 | 1字节 | 24位有符号整数 |
11111110 | 1字节 | 8位有符好整数 |
1111xxxx | 1字节 | encoding的低四位直接保存一个0~12的整数 |
ziplist的添加元素、查找元素、删除元素以及连锁更新的实现源码分析见第二篇博客。
部分理论内容和图片参考/摘自:《Redis设计与实现》——黄健宏