这是我参与8月更文挑战的第9天,活动详情查看:8月更文挑战
ziplist 是 redis 为了节约内存而实现的顺序数据结构。它采取变长编码,将数据+类型+编码信息存储在连续内存中。
先从存储说起:
存储
-
字节数组,一段 连续的内存
-
压缩 ⇒ 整体结构就要足够紧凑:
- header ⇒
[zlbytes, zltail, zllen]→[字节长度,尾部元素偏移量,元素个数] - body data ⇒
[entry1, entry2, ...] - tailer ⇒ 尾部元素,标记结束,恒为
0xFF
- header ⇒
大致的结构如下图:
主要关注 header 的组成:
zlbytes <32 bit>⇒ 占用总字节数zltail <32 bit>⇒ 最后一个entry在整个ziplist的偏移量【存在价值:方便直接遍历到最后一项】→ZIPLIST_TAIL_OFFSETzllen <16 bit>⇒entry数量。因为只有16 bit,如果超过表达范围,那就只能遍历 ziplist 才能知道总计数
我们来看一个例子:
总结一下,这个 ziplist 存储了 4 个元素,这个可以在 zllen 得出。具体存储内容看后续的 entries :
"name" → string"tielei" → string"age" → string20 → int
所以这里就知道表达一个 entry 的 3 个结构的含义:
pre_entry_lenth⇒ 前一项entry占用的 总字节数encoding+len⇒ 编码以及实际内容占用长度【编码:string, int, int16, int65...】content⇒ 具体内容
操作流程
上面看到的是实际存储的结构,这个也对应前面说的连续内存。但是这个结构不适合操作,为什么呢?
- 每次都需要计算前一个元素长度,存储类型,存储内容
- 以上的过程都需要经过复杂的解码计算
- 实际上我们获取的是一个
char *p,但是在程序中需要处理每个部分的编码,自然需要一个struct承接计算的结果
所以为了可以在操作中更为方便,可以将解码的接过缓存到一个固定的结构体中。源码中实际操作的结构体是:zlentry
typedef struct zlentry {
unsigned int prevrawlensize; // 编码 prevrawlen 所需的字节大小
unsigned int prevrawlen; // 前置节点的长度
unsigned int lensize; // 编码 len 所需的字节大小
unsigned int len; // 当前节点值的长度
unsigned int headersize; // 当前节点 header 的大小, 等于prevrawlensize + lensize
unsigned char encoding; // 当前节点值所使用的编码类型
unsigned char *p; // 指向当前节点的指针,也就是内存entry的prelen字段
} zlentry;
我们看看解码的过程,这个也涉及到后续的计算过程:
// p 指向一段内存开始,然后按照特定的编码 -> 解码成 e 这个entry
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); /* check that encoding was valid. */
e->headersize = e->prevrawlensize + e->lensize;
e->p = p;
}
- 解码
previous_entry_length字段 - 解码
encoding字段 - 解码
lensize字段
这个过程和上述讲 entry 结构基本一致。