压缩列表
压缩列表(ziplist)是列表键和哈希键的底层实现之一。当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现。
压缩列表的构成
压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构。一个压缩列表可以包含任意多个节点,每个节点可以保存一个字节数组或者一个整数值。
area |<---- ziplist header ---->|<--- entries -->|<-- end -->|
size 4 bytes 4 bytes 2 bytes 5 bytes 1 bytes
+---------+--------+-------+----------------+-----------+
component | zlbytes | zltail | zllen | entry 1 | zlend |
| | | | | |
value | 10000 | 1010 | 1 | ? | 1111 1111 |
+---------+--------+-------+----------------+-----------+
^ ^
| |
address ZIPLIST_ENTRY_HEAD ZIPLIST_ENTRY_END
&
ZIPLIST_ENTRY_TAIL
- zlbytes记录整个压缩列表占用的内存字节数。
- zltail记录压缩列表表尾节点距离压缩列表的起始地址有多少个字节。
- zllen记录压缩列表的节点数量。
- entryX为压缩列表包含的各个节点,节点的长度由节点保存的内容决定。
- zlend特殊值0XFF,用于标记压缩列表的末端。
同时使用zlentry来保存ziplist节点的信息,要注意的是,zlentry并不是上文中的entry
typedef struct zlentry {
// prevrawlen :前置节点的长度
// prevrawlensize :编码 prevrawlen 所需的字节大小
unsigned int prevrawlensize, prevrawlen;
// len :当前节点值的长度
// lensize :编码 len 所需的字节大小
unsigned int lensize, len;
// 当前节点 header 的大小
// 等于 prevrawlensize + lensize
unsigned int headersize;
// 当前节点值所使用的编码类型
unsigned char encoding;
// 指向当前节点的指针
unsigned char *p;
} zlentry;
压缩列表节点的构成
每个压缩列表节点可以保存一个字节数组或者一个整数值,其中,字节数组可以是以下三种长度的其中一种:
- 长度小于等于2的6次方-1字节的字节数组。
- 长度小于等于2的14次方-1字节的字节数组。
- 长度小于等于2的32次方-1字节的字节数组。
而整数值则可以是以下六种长度的其中一种:
- 4位长,介于0至12之间的无符号整数。
- 1字节长的有符号整数。
- 3字节长的有符号整数。
- int16_t类型整数。
- int32_t类型整数。
- int64_t类型的整数。
每个压缩列表节点都由previous_entry_length、encoding、content三个属性构成。
previous_entry_length从名字就可以看出,它记录了压缩列表前一个节点的长度。该属性可以是1字节或者5字节,当前一个字节长度小于254字节时,该属性就用1字节保存前节点的长度;反之则使用5字节保存前节点的长度,需要注意的是这五个字节的第一字节设置为0XFE(54),之后的四位用来保存前节点长度。
encoding属性记录了节点的content属性所保存数据的类型以及长度:
- 当data小于63字节时(2^6),节点存为上图的第一种类型,高2位为00,低6位表示data的长度。
- 当data小于16383字节时(2^14),节点存为上图的第二种类型,高2位为01,后续14位表示data的长度。
- 当data小于4294967296字节时(2^32),节点存为上图的第二种类型,高2位为10,下一字节起连续32位表示data的长度。
连锁更新
因为压缩列表每个节点的previous_entry_length属性都记录了前一个节点的长度,并且该属性会使用1字节或5字节的空间,那么如果有很多个节点,并且他们的长度都在250到254之间,那么如果在第一个节点之前添加一个长度大于254的节点,那么原来的第一个节点的previous_entry-length就需要变成五个字节,那么该节点的长度就超过了254,那么原来的第二个节点的previous_entry-length也要跟着变化,这么一来程序需要频繁的对压缩列表节点进行空间重分配,Redis将在这种情况下产生的连续多次空间扩展称之为“连锁更新”。当然,除了添加节点以外,删除节点也会引发连锁更新,这种情况还是很容易想象出来的,这里就不再细说了。
因为连锁更新最坏情况下需要对压缩列表所有的N个节点进行空间重分配,每次空间重分配的最坏时间复杂度为O(N),所以连锁更新的最坏时间复杂度为O(N方)。
虽然连锁更新的复杂度较高,但是真正会产生连锁更新的情况并不会常发生,所以可以放心使用。
源码分析
压缩列表的创建:
unsigned char *ziplistNew(void) {
unsigned int bytes = ZIPLIST_HEADER_SIZE+1;//<zlbytes>4字节<zltail>4字节<zllen>2字节<zlend>1字节,没有entry节点
unsigned char *zl = zmalloc(bytes);
ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);//<zlbytes>赋值
ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);//<zltail>
ZIPLIST_LENGTH(zl) = 0;//<zllen>
zl[bytes-1] = ZIP_END;//<zlend>
return zl;
}
#define ZIPLIST_HEADER_SIZE (sizeof(uint32_t)*2+sizeof(uint16_t))//空ziplist除了<zlend>的大小
#define ZIPLIST_BYTES(zl) (*((uint32_t*)(zl)))//<zlbyte>的指针的值,可读可写
#define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t))))//<zltail>的指针的值
#define ZIPLIST_HEADER_SIZE (sizeof(uint32_t)*2+sizeof(uint16_t))//空ziplist除了<zlend>的大小
#define ZIPLIST_LENGTH(zl) (*((uint16_t*)((zl)+sizeof(uint32_t)*2)))//<zllen>的指针的值
分配了一块内存并初始化,没有初始化entry。
压缩列表的查找:
该方法的作用是寻找节点值和 vstr 相等的列表节点,并返回该节点的指针。
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);
ZIP_DECODE_LENGTH(p + prevlensize, encoding, lensize, len);
q = p + prevlensize + lensize;//当前节点的data
if (skipcnt == 0) {
/* Compare current entry with specified entry */
if (ZIP_IS_STR(encoding)) {//判断当前节点是不是字符串节点
if (len == vlen && memcmp(q, vstr, vlen) == 0) {
return p;
}
} else {
/* Find out if the searched field can be encoded. Note that
* we do it only the first time, once done vencoding is set
* to non-zero and vll is set to the integer value. */
if (vencoding == 0) {//这个代码块只会执行一次,计算vstr的整数表示
if (!zipTryEncoding(vstr, vlen, &vll, &vencoding)) {
//将参数给的节点vstr当做整数节点转换;将data值返回给vll,节点编码返回给vencoding
//进入这个代码块说明将vstr转换成整数失败,vencoding不变,下次判断当前节点是整数节点之后可以跳过这个节点
/* If the entry can't be encoded we set it to
* UCHAR_MAX so that we don't retry again the next
* time. */
vencoding = UCHAR_MAX;//当前节点是整数节点,但是vstr是字符串节点,跳过不用比较了
}
/* Must be non-zero by now */
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);//算出当前节点的data
if (ll == vll) {
return p;
}
}
}
/* Reset skip count */
skipcnt = skip;
} else {
/* Skip entry */
skipcnt--;
}
/* Move to next entry */
p = q + len;
}
return NULL;
}
//尝试将entry地址的内容转换成整数,并根据这个整数算出一个合适的encoding返回给encoding参数。
//若无法转换成整数,则encoding不变,返回0,等到下次调用zipEncodeLength时再计算一个该字符串的encoding
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. */
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;
}
*v = value;
return 1;
}
return 0;
}
/* Read integer encoded as 'encoding' from 'p' */
int64_t zipLoadInteger(unsigned char *p, unsigned char encoding) {
int16_t i16;
int32_t i32;
int64_t i64, ret = 0;
if (encoding == ZIP_INT_8B) {
ret = ((int8_t*)p)[0];
} else if (encoding == ZIP_INT_16B) {
memcpy(&i16,p,sizeof(i16));
memrev16ifbe(&i16);
ret = i16;
} else if (encoding == ZIP_INT_32B) {
memcpy(&i32,p,sizeof(i32));
memrev32ifbe(&i32);
ret = i32;
} else if (encoding == ZIP_INT_24B) {
i32 = 0;
memcpy(((uint8_t*)&i32)+1,p,sizeof(i32)-sizeof(uint8_t));
memrev32ifbe(&i32);
ret = i32>>8;
} else if (encoding == ZIP_INT_64B) {
memcpy(&i64,p,sizeof(i64));
memrev64ifbe(&i64);
ret = i64;
} else if (encoding >= ZIP_INT_IMM_MIN && encoding <= ZIP_INT_IMM_MAX) {
ret = (encoding & ZIP_INT_IMM_MASK)-1;
} else {
assert(NULL);
}
return ret;
}