压缩列表(ziplist)是列表和哈希的底层实现之一,是为尽可能地节约内存而设计的特殊编码双端链表。 当一个列表只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现。
压缩列表的优点是节省内存,缺点是插入元素的复杂度较高平均O(N)最坏O(N^2)
, 但是在小数据量的情况下,这种复杂度也是可以接受的。
ziplist 结构
压缩列表是由一系列entry
组成的结构。entry
记录了当前节点的大小和前置节点的大小,所以可以双向插入和遍历。
ziplist 又4个主要部分组成
- zlbytes: 4字节,表示整个列表占用内存大小
- zltail: 4字节,表示列表尾节点相对列表第一个字节的偏移量
- zllen: 2字节,表示列表元素个数,如果节点个数超出2^16-1则需要遍历列表求出元素个数。
- zlend: 1字节
0xFF
, 表示列表末尾
ziplist 结构示例图
1 2 3 4 5 6 7 8 9 10 11
| area |<---- ziplist header ---->|<----------- entries ------------->|<-end->|
size 4 bytes 4 bytes 2 bytes ? ? ? ? 1 byte +---------+--------+-------+--------+--------+--------+--------+-------+ component | zlbytes | zltail | zllen | entry1 | entry2 | ... | entryN | zlend | +---------+--------+-------+--------+--------+--------+--------+-------+ ^ ^ ^ address | | | ZIPLIST_ENTRY_HEAD | ZIPLIST_ENTRY_END | ZIPLIST_ENTRY_TAIL
|
复制

-w1224
上图我们向ziplist添加了3个entry元素,向list头部插入(redis内部使用时一般向尾部插入),后面会详细解析这些元素。
ziplist 节点元素
每个ziplist节点由一下3个部分组成
- prelen: 前一个节点长度,单位为字节
- encoding: 节点的编码类型,
- content: 节点内容,可能是字节数组(c语言字符串去除末尾的\0)或者数组
之前的3个节点的二进制详情

-w678
节点迭代器结构体 zlentry
1 2 3 4 5 6 7 8 9
| 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;
|
复制
注意:zlentry结构体和ziplist中实际存储的entry结构是不一样的,zlentry只是为了遍历时操作entry时便利一些,类似序列化和反序列化。在需要对entry操作时,把对应位置的信息取出存到zlentry结构体中
prelen
prelen 记录了以字节为单位的前一个节点长度,有两种情况
- 默认占用1字节空间,表示0到253
- 如果节点长度大于253,则这个字节就设置为254(0xFE)作为标志位, 随后的4个字节存储实际长度。
255这个数字为啥舍弃不用呢?因为255已经作为列表结束的标志位,避免出现误导。
encoding
encoding 记录了当前节点的编码类型,编码时先尝试将内容转成数字,失败则当做字符串处理。
个人觉得ziplist的精华就在entry的encoding,对让内存的每一个bit都重复表示了信息。
下表中的0和1表示具体的二进制位, b表示该位置可能为0或者1
编码 |
占用空间/字节 |
表示类型 |
具体含义 |
00bbbbbb |
1 |
字节数组 |
content的长度为6bit, 也就是0-63 |
01bbbbbb bbbbbbbb |
2 |
字节数组 |
content的长度为14bit, 也就是0-16383 |
10000000 bbbbbbbb bbbbbbbb bbbbbbbb bbbbbbbb |
5 |
字节数组 |
content的长度为32bit, 也就是0-4294967295 |
11110001 到 11111101 |
1 |
数字 |
用4个bit直接表示数字0-12, content长度为0 |
11111110 |
1 |
数字 |
content为int8_t, 长度2字节 |
11000000 |
1 |
数字 |
content为int16_t, 长度2字节 |
11010000 |
1 |
数字 |
content为int32_t, 长度4字节 |
11100000 |
1 |
数字 |
content为int64_t, 长度8字节 |
11110000 |
1 |
数字 |
content为24bit有符号整数, 长度3字节 |
可以看到ziplist为了节省内存空间,表示信息时真是细扣到每一个bit,非常高效。
但是也有个不足,就是代码变得复杂了。
由于prelen和encoding和content这3个部分都是变长的,每一次插入和删除元素都得计算列表内存长度的变化。
而且由于prelen的变长,可能会触发后面所有节点连锁更新prelen的值.
本来节点插入时只需要复制一次该节点以后所有节点的内存,这时复杂度为O(n), 触发连锁更新之后,这时候列表的插入复杂度就会变为O(n^2)。
list插入
当list底层实现为ziplist时,插入原始的逻辑
主要涉及到各种长度和偏移量的计算,比较繁琐
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
| static unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) { size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), reqlen, prevlen = 0; size_t offset; int nextdiff = 0; unsigned char encoding = 0; long long value = 123456789; zlentry entry, tail;
if (p[0] != ZIP_END) { entry = zipEntry(p); prevlen = entry.prevrawlen; } else { unsigned char *ptail = ZIPLIST_ENTRY_TAIL(zl); if (ptail[0] != ZIP_END) { prevlen = zipRawEntryLength(ptail); } }
if (zipTryEncoding(s,slen,&value,&encoding)) { reqlen = zipIntSize(encoding); } else { reqlen = slen; } reqlen += zipPrevEncodeLength(NULL,prevlen); reqlen += zipEncodeLength(NULL,encoding,slen);
nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0;
offset = p-zl; zl = ziplistResize(zl,curlen+reqlen+nextdiff); p = zl+offset;
if (p[0] != ZIP_END) { memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff); zipPrevEncodeLength(p+reqlen,reqlen); ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+reqlen); tail = zipEntry(p+reqlen); 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); }
if (nextdiff != 0) { offset = p-zl; zl = __ziplistCascadeUpdate(zl,p+reqlen); p = zl+offset; }
p += zipPrevEncodeLength(p,prevlen); p += zipEncodeLength(p,encoding,slen); if (ZIP_IS_STR(encoding)) { memcpy(p,s,slen); } else { zipSaveInteger(p,value,encoding); } ZIPLIST_INCR_LENGTH(zl,1); return zl; }
|
复制
list底层转换为链表
判断是不是要把list转换为链表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| void listTypeTryConversion(robj *subject, robj *value) { if (subject->encoding != REDIS_ENCODING_ZIPLIST) return; if (sdsEncodedObject(value) && sdslen(value->ptr) > server.list_max_ziplist_value) listTypeConvert(subject,REDIS_ENCODING_LINKEDLIST); }
void listTypeConvert(robj *subject, int enc) { listTypeIterator *li; listTypeEntry entry; if (enc == REDIS_ENCODING_LINKEDLIST) { list *l = listCreate(); listSetFreeMethod(l,decrRefCountVoid); li = listTypeInitIterator(subject,0,REDIS_TAIL); while (listTypeNext(li,&entry)) listAddNodeTail(l,listTypeGet(&entry)); listTypeReleaseIterator(li); subject->encoding = REDIS_ENCODING_LINKEDLIST; zfree(subject->ptr); subject->ptr = l; } else { redisPanic("Unsupported list conversion"); } }
|
复制
参考
Gitalking ...