本文讲解 redis 中的 ziplist 数据结构。涉及到的代码文件有 src/ziplist.c src/ziplist.h
关于 ziplist 的实现,源代码中作者添加了非常详细明了的注释,这些注释会成为我们后面学习这份代码的臂助。
what
/*
*The ziplist is a special encoded dually linked list that is designed to be very memory efficent.
*It stores both strings and integer values, where intgers are encoded as acutal integers instead of a series of characters.
*/
ziplist 是一个用来存储字符串的、经过内存占用优化的双向链表。因为这个内存优化,我们会
看到这个 ziplist 一点都不像链表,反而像一个数组。
how
overview
我们先来看看 ziplist 的大体布局:
/*
* ZIPLIST OVERALL LAYOUT:
*
* The general layout of the ziplist is as follows:
*
* <zlbytes><zltail><zllen><entry><entry><zlend>
*
* <zlbytes> is an unsigned integer to hold the number of bytes that the
* ziplist occupies. This value needs to be stored to be able to resize the
* entire structure without the need to traverse it first.
*
* <zltail> is the offset to the last entry in the list. This allows a pop
* operation on the far side of the list without the need for full traversal.
*
* <zllen> is the number of entries.When this value is larger than 2**16-2,
* we need to traverse the entire list to know how many items it holds.
*
* <zlend> is a single byte special value, equal to 255, which indicates the
* end of the list.
*/
按照注释说明,代码中定义了如下宏用于获取 ziplist 中的字段:
// zlbytes uint32_t
#define ZIPLIST_BYTES(zl) (*((uint32_t *)(zl)))
// zloffset uint32_t
#define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t *)((zl) + sizeof(uint32_t))))
// zllength uint16_t
#define ZIPLIST_LENGTH(zl) (*((uint16_t *)((zl) + sizeof(uint32_t) * 2)))
// zlend 为固定的 255(1byte),这个暗示我们在 ziplist 中,除了 zlend,不会出现 255 这个 byte
#define ZIP_END 255
// ziplist header 大小,包括 zlbytes,zloffset,zlend
#define ZIPLIST_HEADER_SIZE (sizeof(uint32_t) * 2 + sizeof(uint16_t))
有了这几个宏,我们就不难理解构造函数了:
unsigned char *ziplistNew(void) {
// ZIPLIST_HEADER_SIZE 是 ziplist 表头的大小
// 1 字节是表末端 ZIP_END 的大小
unsigned int bytes = ZIPLIST_HEADER_SIZE + 1;
// 为表头和表末端分配空间
unsigned char *zl = zmalloc(bytes);
// 初始化表属性, 注意 interv32ifbe,因为我们所有的整数都是用
// 小端表示,这个函数会在当机器是大端表示时进行转换(对于本身
// 是小端表示的机器则不做任何处理)
ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);
ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);
ZIPLIST_LENGTH(zl) = 0;
// 设置表末端
zl[bytes - 1] = ZIP_END;
return zl;
}
从构造函数我们可以看到,其实并不存在一个 ziplist 结构体,其只是一片
连续内存而已。
下面我们再来看几个宏的定义,可以帮我们快速定位到头部和尾部的 entry
/*
首先来看一个非空 ziplist 的示例图
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
*/
// 指向第一个 entry 的指针
#define ZIPLIST_ENTRY_HEAD(zl) ((zl) + ZIPLIST_HEADER_SIZE)
// 指向最后一个 entry 的指针,还记得前面说的 zloffset 记录的是最后一个 entry 的偏移吧?
#define ZIPLIST_ENTRY_TAIL(zl) ((zl) + intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl)))
// zlend 的位置(zlbytes-1)
#define ZIPLIST_ENTRY_END(zl) ((zl) + intrev32ifbe(ZIPLIST_BYTES(zl)) - 1)
entry
通过前面的布局图我们看到了,ziplist 内存储这若干个 entry,来看一下 entry 的定义
/*
* Every entry in the ziplist is prefixed by a header that contains two pieces
* of information. First, the length of the previous entry is stored to be
* able to traverse the list from back to front. Second, the encoding with an
* optional string length of the entry itself is stored.
* 一个 zlentry 的内存布局如下
+--------------------------+--------------------------+----------------+
component | prevlen encoding data | entrylen encoding data |underlaying data|
+--------------------------+--------------------------+----------------+
*/
typedef struct zlentry {
// prevrawlen :前置节点的长度
// prevrawlensize :编码 prevrawlen 所需的字节大小
unsigned int prevrawlensize, prevrawlen; // 从 prevlen encoding data 解析得到
// len :当前节点值的长度
// lensize :编码 len 所需的字节大小
unsigned int lensize, len; // --->|
// 当前节点值所使用的编码类型 // | 从 entrylen encoding data 解析得到
unsigned char encoding; // --->|
// 当前节点 header 的大小
// 等于 prevrawlensize + lensize
unsigned int headersize;
// 指向当前节点编码后的起始地址
unsigned char *p;
} zlentry;
关于 ziplist 比较复杂的地方在于,虽然在布局图上我们画出了一个个 entry,但是
在 ziplist 实际中存储的,并不是上面定义的 zlentry 结构体。而是 zlentry 经过
特定编码格式以后的一个 unsigned char。通过将lzentry
encoding,可以减少内存的占用。
先看一下 prevlen 的编码方式:
/*
* The length of the previous entry is encoded in the following way:
* If this length is smaller than 254 bytes, it will only consume a single
* byte that takes the length as value. When the length is greater than or
* equal to 254, it will consume 5 bytes. The first byte is set to 254 to
* indicate a larger value is following. The remaining 4 bytes take the
* length of the previous entry as value.
即 prevlen 有两种编码格式分别占用 1 byte 和 5 bytes,用于表示小与 254 和
大于 254 的 unsinged int
1). prevlen < 254
size 1 byte
+---------+
component | prevlen |
+---------+
2). prevlen > 254
size 1 byte 4 bytes
+---------+--------+
component | 254 | prevlen|
+---------+--------+
*/
#define ZIP_BIGLEN 254
// prevlen 编码函数
static unsigned int zipPrevEncodeLength(unsigned char *p, unsigned int len) {
// 仅返回编码 len 所需的字节数量
if (p == NULL) {
return (len < ZIP_BIGLEN) ? 1 : sizeof(len) + 1;
// 写入并返回编码 len 所需的字节数量
} else {
// 1 字节
if (len < ZIP_BIGLEN) {
p[0] = len;
return 1;
// 5 字节
} else {
// 添加 5 字节长度标识
p[0] = ZIP_BIGLEN;
// 写入编码
memcpy(p + 1, &len, sizeof(len));
// 如果有必要的话,进行大小端转换
memrev32ifbe(p + 1);
// 返回编码长度
return 1 + sizeof(len);
}
}
}
// prevlen 解码使用宏
#define ZIP_DECODE_PREVLENSIZE(ptr, prevlensize) \
do { \
if ((ptr)[0] < ZIP_BIGLEN) { \
(prevlensize) = 1; \
} else { \
(prevlensize) = 5; \
} \
} while (0);
#define ZIP_DECODE_PREVLEN(ptr, prevlensize, prevlen) \
do { \
\
/* 先计算被编码长度值的字节数 */ \
ZIP_DECODE_PREVLENSIZE(ptr, prevlensize); \
\
/* 再根据编码字节数来取出长度值 */ \
if ((prevlensize) == 1) { \
(prevlen) = (ptr)[0]; \
} else if ((prevlensize) == 5) { \
assert(sizeof((prevlensize)) == 4); \
memcpy(&(prevlen), ((char *)(ptr)) + 1, 4); \
memrev32ifbe(&prevlen); \
} \
} while (0);
上面的两个宏的逻辑配合注释很容易理解。主要是编码不同大小的整数时候使用
不同的编码方式。在处理小于 254 的整数时,我们只使用了 1byte,节省了3bytees,
虽然在对于大于 254 的整数浪费了一个字节,但是因为其实在大多数程序中,对于小
整数的使用频率是远远高于大整数的。所以使用这种编码在一般可以节省很多空间
(相同思想的还有 google 的 varint)。
节点本身编码:
/*
* The other header field of the entry itself depends on the contents of the
* entry. When the entry is a string, the first 2 bits of this header will hold
* the type of encoding used to store the length of the string, followed by the
* actual length of the string. When the entry is an integer the first 2 bits
* are both set to 1. The following 2 bits are used to specify what kind of
* integer will be stored after this header. An overview of the different
* types and encodings is as follows(The first byte is always enough
* to determine the kind of entry):
* |00pppppp| - 1 byte
* String value with length less than or equal to 63 bytes (6 bits).
* "pppppp" represents the unsigned 6 bit length.
* |01pppppp|qqqqqqqq| - 2 bytes
* String value with length less than or equal to 16383 bytes (14 bits).
* IMPORTANT: The 14 bit number is stored in big endian.
* |10000000|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt| - 5 bytes
* String value with length greater than or equal to 16384 bytes.
* Only the 4 bytes following the first byte represents the length
* up to 32^2-1. The 6 lower bits of the first byte are not used and
* are set to zero.
* IMPORTANT: The 32 bit number is stored in big endian.
* |11000000| - 3 bytes
* Integer encoded as int16_t (2 bytes).
* |11010000| - 5 bytes
* Integer encoded as int32_t (4 bytes).
* |11100000| - 9 bytes
* Integer encoded as int64_t (8 bytes).
* |11110000| - 4 bytes
* Integer encoded as 24 bit signed (3 bytes).
* |11111110| - 2 bytes
* Integer encoded as 8 bit signed (1 byte).
* |1111xxxx| - (with xxxx between 0000 and 1101) immediate 4 bit integer.
* Unsigned integer from 0 to 12. The encoded value is actually from
* 1 to 13 because 0000 and 1111 can not be used, so 1 should be
* subtracted from the encoded 4 bit value to obtain the right value.
* |11111111| - End of ziplist special entry.
*/
#define ZIP_STR_MASK 0xc0 // 上面注释指出 string 只有三种编码方式分别以 00、01、10 开头
#define ZIP_INT_MASK 0x30
#define ZIP_STR_06B (0 << 6)
#define ZIP_STR_14B (1 << 6)
#define ZIP_STR_32B (2 << 6)
/*
* 整数编码类型
*/
#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
#define ZIP_IS_STR(enc) (((enc)&ZIP_STR_MASK) < ZIP_STR_MASK)
// entry encoding
static unsigned int zipEncodeLength(unsigned char *p, unsigned char encoding,
unsigned int rawlen) {
unsigned char len = 1, buf[5];
// 编码字符串
if (ZIP_IS_STR(encoding)) {
/* Although encoding is given it may not be set for strings,
* so we determine it here using the raw length. */
if (rawlen <= 0x3f) {
if (!p)
return len;
buf[0] = ZIP_STR_06B | rawlen;
} else if (rawlen <= 0x3fff) {
len += 1;
if (!p)
return len;
buf[0] = ZIP_STR_14B | ((rawlen >> 8) & 0x3f);
buf[1] = rawlen & 0xff;
} else {
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 {
/* Implies integer encoding, so length is always 1. */
if (!p)
return len;
buf[0] = encoding;
}
/* Store this length at p */
// 将编码后的长度写入 p
memcpy(p, buf, len);
// 返回编码所需的字节数
return len;
}
// entry decoding
#define ZIP_ENTRY_ENCODING(ptr, encoding) \
do { \
(encoding) = (ptr[0]); \
if ((encoding) < ZIP_STR_MASK) \
(encoding) &= ZIP_STR_MASK; \
} while (0)
#define ZIP_DECODE_LENGTH(ptr, encoding, lensize, len) \
do { \
\
/* 取出值的编码类型 */ \
ZIP_ENTRY_ENCODING((ptr), (encoding)); \
\
if ((encoding) < ZIP_STR_MASK) { \
/* 字符串编码 */ \
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 { \
assert(NULL); \
} \
} else { \
/* 整数编码 */ \
(lensize) = 1; \
(len) = zipIntSize(encoding); \
} \
} while (0);
static unsigned int zipIntSize(unsigned char encoding) {
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;
default:
return 0; /* 4 bit immediate */
}
assert(NULL);
return 0;
}
解码代码结合注释看还是很直观的,我们可以看到 entry 自身信息的编码主要有:
其内容类型(因为 ziplist 里面同时可以存储字符串和整数)和其长度的编码。不难
发现,entry 自身信息的编码因为要加入 encoding 字段,所以比 prevlen 的编码
更为复杂。
有了上面几个 decode 相关辅助函数,我们就可以将一个 zlentry 解码出来了:
zlentry zipEntry(unsigned char *p) {
zlentry e;
// 解码 prevlen 信息
ZIP_DECODE_PREVLEN(p, e->prevrawlensize, e->prevrawlen);
// 解码 entry encoding len 信息
ZIP_DECODE_LENGTH(p + e->prevrawlensize, e->encoding, e->lensize, e->len);
e->headersize = e->prevrawlensize + e->lensize;
e->p = p;
return e;
}
注意这个函数将 zlentry 整个结构体按值返回,具有一定的性能问题,在新版本的 redis
代码中已经修改为按照应用传值(传入指针)。
insert
现在我们来看一下 ziplist 如何进行数据的插入:
// 尝试将字符串编码成整数, 注意这里的 entry 不是 zlentry,值得是要编码的数据
// 比如 "123" 需要占用 3 字节,但是其实按照前面提到的编码方式,我们只用一个字节
// 就够了
static 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)) {
// 转换成功,以从小到大的顺序检查适合值 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;
}
// 计算新旧 prevlen encoding 长度 diff
static int zipPrevLenByteDiff(unsigned char *p, unsigned int len) {
unsigned int prevlensize;
// 取出编码原来的前置节点长度所需的字节数
// T = O(1)
ZIP_DECODE_PREVLENSIZE(p, prevlensize);
// 计算编码 len 所需的字节数,然后进行减法运算
// T = O(1)
return zipPrevEncodeLength(NULL, len) - prevlensize;
}
// 解析节点的长度
static unsigned int zipRawEntryLength(unsigned char *p) {
unsigned int prevlensize, encoding, lensize, len;
// 取出编码前置节点的长度所需的字节数
// T = O(1)
ZIP_DECODE_PREVLENSIZE(p, prevlensize);
// 取出当前节点值的编码类型,编码节点值长度所需的字节数,以及节点值的长度
// T = O(1)
ZIP_DECODE_LENGTH(p + prevlensize, encoding, lensize, len);
// 计算节点占用的字节数总和
return prevlensize + lensize + len;
}
static unsigned char *ziplistResize(unsigned char *zl, unsigned int len) {
// 用 zrealloc ,扩展时不改变现有元素
zl = zrealloc(zl, len);
// 更新 bytes 属性
ZIPLIST_BYTES(zl) = intrev32ifbe(len);
// 重新设置表末端
zl[len - 1] = ZIP_END;
return zl;
}
static void zipPrevEncodeLengthForceLarge(unsigned char *p, unsigned int len) {
if (p == NULL)
return;
// 设置 5 字节长度标识
p[0] = ZIP_BIGLEN;
// 写入 len
memcpy(p + 1, &len, sizeof(len));
memrev32ifbe(p + 1);
}
static 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;
while (p[0] != ZIP_END) {
// 将 p 所指向的节点的信息保存到 cur 结构中
cur = zipEntry(p);
// 当前节点的长度
rawlen = cur.headersize + cur.len;
// 计算编码当前节点的长度所需的字节数
rawlensize = zipPrevEncodeLength(NULL, rawlen);
/* Abort if there is no next entry. */
if (p[rawlen] == ZIP_END)
break;
// 取出后续节点的信息,保存到 next 结构中
next = zipEntry(p + rawlen);
/* Abort when "prevlen" has not changed. */
if (next.prevrawlen == rawlen)
break;
if (next.prevrawlensize < rawlensize) {
// 执行到这里,表示 next 空间的大小不足以编码 cur 的长度
// 所以程序需要对 next 节点的(header 部分)空间进行扩展
// 记录 p 的偏移量
offset = p - zl;
// 计算需要增加的节点数量
extra = rawlensize - next.prevrawlensize;
// 扩展 zl 的大小
zl = ziplistResize(zl, curlen + extra);
// 还原指针 p
p = zl + offset;
// 记录下一节点的偏移量
np = p + rawlen;
noffset = np - zl;
// 当 next 节点不是表尾节点时,更新列表到表尾节点的偏移量
//
// 不用更新的情况(next 为表尾节点):
//
// | | next | ==> | | new next |
// ^ ^
// | |
// tail tail
//
// 需要更新的情况(next 不是表尾节点):
//
// | next | | ==> | new next | |
// ^ ^
// | |
// old tail old tail
//
// 更新之后:
//
// | new next | |
// ^
// |
// new tail
if ((zl + intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))) != np) {
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl)) + extra);
}
/* Move the tail to the back. */
// 向后移动 cur 节点之后的数据,为 cur 的新 header 腾出空间
//
// 示例:
//
// | header | value | ==> | header | | value | ==> | header |
// value |
// |<-->|
// 为新 header 腾出的空间
// T = O(N)
memmove(np + rawlensize, np + next.prevrawlensize,
curlen - noffset - next.prevrawlensize - 1);
// 将新的前一节点长度值编码进新的 next 节点的 header
zipPrevEncodeLength(np, rawlen);
/* Advance the cursor */
// 移动指针,继续处理下个节点
p += rawlen;
curlen += extra;
} else {
if (next.prevrawlensize > rawlensize) {
// 执行到这里,说明 next 节点编码前置节点的 header 空间有 5 字节
// 而编码 rawlen 只需要 1 字节
// 但是程序不会对 next 进行缩小,
// 所以这里只将 rawlen 写入 5 字节的 header 中就算了。
zipPrevEncodeLengthForceLarge(p + rawlen, rawlen);
} else {
// 说明 cur 节点的长度正好可以编码到 next 节点的 header 中
zipPrevEncodeLength(p + rawlen, rawlen);
}
/* Stop here, as the raw length of "next" has not changed. */
break;
}
}
return zl;
}
#define ZIPLIST_INCR_LENGTH(zl, incr) \
{ \
if (ZIPLIST_LENGTH(zl) < UINT16_MAX) \
ZIPLIST_LENGTH(zl) = \
intrev16ifbe(intrev16ifbe(ZIPLIST_LENGTH(zl)) + incr); \
}
static void zipSaveInteger(unsigned char *p, int64_t value,
unsigned char encoding) {
int16_t i16;
int32_t i32;
int64_t i64;
if (encoding == ZIP_INT_8B) {
((int8_t *)p)[0] = (int8_t)value;
} else if (encoding == ZIP_INT_16B) {
i16 = value;
memcpy(p, &i16, sizeof(i16));
memrev16ifbe(p);
} else if (encoding == ZIP_INT_24B) {
i32 = value << 8;
memrev32ifbe(&i32);
memcpy(p, ((uint8_t *)&i32) + 1, sizeof(i32) - sizeof(uint8_t));
} else if (encoding == ZIP_INT_32B) {
i32 = value;
memcpy(p, &i32, sizeof(i32));
memrev32ifbe(p);
} else if (encoding == ZIP_INT_64B) {
i64 = value;
memcpy(p, &i64, sizeof(i64));
memrev64ifbe(p);
} else if (encoding >= ZIP_INT_IMM_MIN && encoding <= ZIP_INT_IMM_MAX) {
/* Nothing to do, the value is stored in the encoding itself. */
} else {
assert(NULL);
}
}
// 在位置 p 处插入数据 s
static unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p,
unsigned char *s, unsigned int slen) {
// 记录当前 ziplist 的长度
size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl));
// 存储 s 需要占用的内存数
size_t reqlen;
size_t prevlen;
size_t offset;
int nextdiff = 0;
unsigned char encoding = 0;
long long value = 123456789; /* initialized to avoid warning. Using a value
that is easy to see if for some reason
we use it uninitialized. */
zlentry entry, tail;
/* Find out prevlen for the entry that is inserted. */
if (p[0] != ZIP_END) {
// 如果 p[0] 不指向列表末端,说明列表非空,并且 p 正指向列表的其中一个节点
// decode 该节点, 这个节点在插入结束后会成为新插入节点的后继节点
entry = zipEntry(p);
prevlen = entry.prevrawlen;
} else {
// 如果 p 指向表尾末端,那么程序需要检查列表是否为:
// 1)如果 ptail 也指向 ZIP_END ,那么列表为空; prevlen = 0
// 2)如果列表不为空,那么 ptail 将指向列表的最后一个节点,
// 需要获取尾节点的长度作为 prevlen
unsigned char *ptail = ZIPLIST_ENTRY_TAIL(zl);
if (ptail[0] != ZIP_END) {
prevlen = zipRawEntryLength(ptail);
}
}
// ziplist 对外提供的接口,在插入的时候传入的都是一个字符串
// 但是如果这个字符串编码成一个整数之后可以节省空间,ziplist
// 会选择将其编码成一个整数插入到 ziplist,这个对调用者是透明的
// 调用者并不知道他调用接口时候插入的 ”123“ 在 ziplist 中是如何存储的
if (zipTryEncoding(s, slen, &value, &encoding)) {
/* 'encoding' is set to the appropriate integer encoding */
reqlen = zipIntSize(encoding);
} else {
// 仍然使用字符串编码
reqlen = slen;
}
/*
+--------------------------+--------------------------+----------------+
component | prevlen encoding data | entrylen encoding data |underlaying data|
+--------------------------+--------------------------+----------------+
计算占用内存,加上 header 占用的长度
*/
reqlen += zipPrevEncodeLength(NULL, prevlen); // prevlen encoding
reqlen += zipEncodeLength(NULL, encoding, slen); // entry encoding
// 如果插入的位置不是在链表尾部,那么我们新插入的节点就会有后继节点
// 后继节点保存的 prevlen 信息就需要更新,而且因为长度变化,可能存在
// 之前 prevlen < 254 所以只占用了 1 byte,我们新插入的 entry 占用
// 超过 254 字节,占用 5 byte(反过来5 byte -> 1 byte 也一样)
// 在这里我们计算一下新旧 prevlen 的 diff
nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p, reqlen) : 0;
// 因为 resize 后 zl 地址可能改变,到时候 p-zl 的结果会变为非法,所以
// 先计算 offset,用于 resize 之后重新索引 p
offset = p - zl;
zl = ziplistResize(zl, curlen + reqlen + nextdiff);
p = zl + offset;
/* Apply memory move when necessary and update tail offset. */
if (p[0] != ZIP_END) {
// 最开始已经提过了,ziplist 其实就像一个数组,要在数组中间插入数据,需要
// 搬运数据
/*
area |<--------------offset------------->|<------curl-offset-1+nextdiff----->|
+---------+--------+-------+--------+--------+--------+--------+--------+------+
component | zlbytes | zltail | zllen | entry1 | entry2 | ... | new empty buffer|zlend |
+---------+--------+-------+--------+--------+--------+--------+--------+------+
^-newentry-^
address | |
p p+reqlen
我们需要将 p 之后的数据搬运到 p+reqlen 处, 需要注意的是我们要给从 p-nextdiff 处开始搬运
要不然可能导致内存越界(大家可以按照 nextdiff == 4 计算一下)
*/
memmove(p + reqlen, p - nextdiff, curlen - offset - 1 + nextdiff);
// 更新后继节点的 prevlen
zipPrevEncodeLength(p + reqlen, reqlen);
// 因为是在中间插入,所以尾节点 offset 需要加上 reqlen
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl)) + reqlen);
// 这里有个 corner case,那就是如果我们的后继节点不是是尾节点
// 那么因为后继节点的长度也可能发生了改变,我们需要将 nextdiff
// 也加到尾节点 offset 中
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 {
// 插入的是尾节点,更新 tail offset
ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(p - zl);
}
// 当 nextdiff != 0 时,新节点的后继节点的(header 部分)长度已经修改,
// 所以需要级联地更新后续的节点存储的 prevlen
if (nextdiff != 0) {
offset = p - zl;
zl = __ziplistCascadeUpdate(zl, p + reqlen);
p = zl + offset;
}
// 现在只需要将将新 zlentry 编码即可
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;
}
上面的插入例程逻辑较为复杂,主要是因为其有大量编、解码逻辑,而且后继节点存储
了前驱节点的信息(prevlen)所以导致可能牵一发而动全身的情况,所以有级联更新的
逻辑,而且在极端的情况下,级联更新的时间复杂度可以达到 O(n^2) !
ziplist 支持在头、尾插入元素,其实现都是委托 __ziplistInsert 实现的:
unsigned char *ziplistPush(unsigned char *zl, unsigned char *s,
unsigned int slen, int where) {
// 根据 where 参数的值,决定将值推入到表头还是表尾
unsigned char *p;
p = (where == ZIPLIST_HEAD) ? ZIPLIST_ENTRY_HEAD(zl) : ZIPLIST_ENTRY_END(zl);
// 返回添加新值后的 ziplist
return __ziplistInsert(zl, p, s, slen);
}
delete
下面再来看一下 delete 的实现, 了解了 insert 的逻辑,想必其逆操作难不倒我们:
// 从 p 开始,连续删除 num 个节点
static 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;
// 计算被删除节点总共占用的内存字节数
// 以及被删除节点的总个数
first = zipEntry(p);
for (i = 0; p[0] != ZIP_END && i < num; i++) {
p += zipRawEntryLength(p);
deleted++;
}
// totlen 是所有被删除节点总共占用的内存字节数
totlen = p - first.p;
if (totlen > 0) {
if (p[0] != ZIP_END) {
// 执行这里,表示被删除节点之后仍然有节点存在
// 与插入逻辑一样,因为 p 的前驱节点发生了改变,所以我们要
// 看看是不是会导致其占用内存发生变化
nextdiff = zipPrevLenByteDiff(p, first.prevrawlen);
// 如果有需要的话,将指针 p 后退 nextdiff 字节,为新 header 空出空间
p -= nextdiff;
// 删除后 p 的前驱节点变为 first 的前驱,所以将 p 的 prevlen 设为 firstl.prevrawlen
zipPrevEncodeLength(p, first.prevrawlen);
// 更新 ziplist tail offset 的逻辑与插入时一致,不再赘述
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl)) - totlen);
tail = zipEntry(p);
if (p[tail.headersize + tail.len] != ZIP_END) {
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl)) + nextdiff);
}
// 从表尾向表头移动数据,覆盖被删除节点的数据
memmove(first.p, p, intrev32ifbe(ZIPLIST_BYTES(zl)) - (p - zl) - 1);
} else {
// 执行这里,表示被删除节点之后已经没有其他节点了
ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe((first.p - zl) - first.prevrawlen);
}
// 缩小并更新 ziplist 的长度
offset = first.p - zl;
zl = ziplistResize(zl, intrev32ifbe(ZIPLIST_BYTES(zl)) - totlen + nextdiff);
ZIPLIST_INCR_LENGTH(zl, -deleted);
p = zl + offset;
// 如果 p 所指向的节点的大小已经变更,那么进行级联更新
// 检查 p 之后的所有节点是否符合 ziplist 的编码要求
if (nextdiff != 0)
zl = __ziplistCascadeUpdate(zl, p);
}
return zl;
}
读接口(们)
现在我们来看一下读相关接口。既然 ziplist 是一个双向链表,那么我们先看一下如何通过 p
查找其前驱和后继节点
// 给定 p 返回其后继节点
unsigned char *ziplistNext(unsigned char *zl, unsigned char *p) {
((void)zl);
// p 已经指向列表末端
if (p[0] == ZIP_END) {
return NULL;
}
// 指向后一节点
p += zipRawEntryLength(p);
if (p[0] == ZIP_END) {
// p 已经是表尾节点,没有后置节点
return NULL;
}
return p;
}
// 给定 p 返回其前驱节点
unsigned char *ziplistPrev(unsigned char *zl, unsigned char *p) {
zlentry entry;
if (p[0] == ZIP_END) {
// 如果 p 指向列表末端(列表为空,或者刚开始从表尾向表头迭代)
// 那么尝试取出列表尾端节点
p = ZIPLIST_ENTRY_TAIL(zl);
// 尾端节点也指向列表末端,那么列表为空
return (p[0] == ZIP_END) ? NULL : p;
} else if (p == ZIPLIST_ENTRY_HEAD(zl)) {
// 如果 p 指向列表头 没有前驱节点
return NULL;
} else {
// 既不是表头也不是表尾,从表尾向表头移动指针
// 计算前一个节点的节点数
entry = zipEntry(p);
assert(entry.prevrawlen > 0);
// 移动指针,指向前一个节点
return p - entry.prevrawlen;
}
}
查询一个节点的值,注意,虽然我们的 push 方法并不提供一个 push 整数的接口
但是在 get 的时候,却会根据在 ziplist 中的 encoding 模式,返回字符串或者
整数:
unsigned int ziplistGet(unsigned char *p, unsigned char **sstr,
unsigned int *slen, long long *sval) {
zlentry entry;
if (p == NULL || p[0] == ZIP_END)
return 0;
if (sstr)
*sstr = NULL;
// 取出 p 所指向的节点的各项信息,并保存到结构 entry 中
entry = zipEntry(p);
if (ZIP_IS_STR(entry.encoding)) {
// 节点的值为字符串,将字符串长度保存到 *slen ,字符串保存到 *sstr
if (sstr) {
*slen = entry.len;
*sstr = p + entry.headersize;
}
} else {
// 节点的值为整数,解码值,并将值保存到 *sval
if (sval) {
*sval = zipLoadInteger(p + entry.headersize, entry.encoding);
}
}
return 1;
}
ziplist 还提供了 compare 接口将 entry 与字符串进行对比:
unsigned int ziplistCompare(unsigned char *p, unsigned char *sstr,
unsigned int slen) {
zlentry entry;
unsigned char sencoding;
long long zval, sval;
if (p[0] == ZIP_END)
return 0;
// 取出节点
entry = zipEntry(p);
if (ZIP_IS_STR(entry.encoding)) {
// 节点值为字符串,进行字符串对比, 小窍门先检查长度是否相等
// 对于不想等的可以避免整个字符串的比较
if (entry.len == slen) {
return memcmp(p + entry.headersize, sstr, slen) == 0;
} else {
return 0;
}
} else {
// 节点值为整数,尝试将 sstr 也编码成整数
if (zipTryEncoding(sstr, slen, &sval, &sencoding)) {
zval = zipLoadInteger(p + entry.headersize, entry.encoding);
return zval == sval;
}
}
return 0;
}
最后是一个略微有点奇怪的 Find 函数,它可以设置一个 skip 参数来让每次比较
中间隔 skip 个节点:
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;
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 {
// 对 tryEncoding 的结果进行缓存
// 1.encoding 失败,vencoding=UCHAR_MAC
// 2.encoding 成果,encoding 结果已经保存在 vll,有 vencoding!=UCHAR_MAX
if (vencoding == 0) {
if (!zipTryEncoding(vstr, vlen, &vll, &vencoding)) {
vencoding = UCHAR_MAX;
}
assert(vencoding);
}
if (vencoding != UCHAR_MAX) {
long long ll = zipLoadInteger(q, encoding);
if (ll == vll) {
return p;
}
}
}
skipcnt = skip;
} else {
skipcnt--;
}
// 前进到下一个节点
p = q + len;
}
// 没有找到指定的节点
return NULL;
}
至此 ziplist 的相关代码我们就学习完成了。ziplist 在存储一些形如:“123412” 的字符串表示的整数时候,可以节省内存。有其特定的试用场景