listpack 出现之前,redis 是使用 ziplist 的。7.0 后,ziplist 就已经弃用了。如今虽然还能在 7.4 中看到 ziplist 的代码,但是好像确实没有哪里是调用它的了。
为什么要更换到 listpack?以下是作者的原话,也可以见该 issue: https://github.com/redis/redis/issues/8702
大概意思是说 ziplist 会有级联更新。具体为什么我不了解,因为我没有读,而且已经弃用了。这个 issue 标题是 [NEW] listpack migration - replace all usage of ziplist with listpack
中文翻译就是替换所有的 ziplist 为 listpack,所以说弃用似乎没有问题。
listpack 没有专门的类型,因为每一个元素大小都不一样,所以它就直接用一个unsigned char *
表示。所以创建它的函数是:
unsigned char *lpNew(size_t capacity) {
unsigned char *lp = lp_malloc(capacity > LP_HDR_SIZE+1 ? capacity : LP_HDR_SIZE+1);
if (lp == NULL) return NULL;
lpSetTotalBytes(lp,LP_HDR_SIZE+1);
lpSetNumElements(lp,0);
lp[LP_HDR_SIZE] = LP_EOF;
return lp;
}
估计用 unsigned 的原因是警告用户这个指针不是普通的字符串。
由于 listpack 没有对应的结构体,与其聊它是如何增删改查的,还不如说说它每个元素是如何编码的。
listpack 编码
header
listpack 有一个 header,分别是一个 32 位整数,表示该压缩列表一共分配了多少空间,一个 16 位整数,表示它存储了多少个元素。以下宏函数,第一个是获得一共分配了多少空间,第二个是获得一共存储了多少个元素。
#define lpGetTotalBytes(p) (((uint32_t)(p)[0]<<0) | \
((uint32_t)(p)[1]<<8) | \
((uint32_t)(p)[2]<<16) | \
((uint32_t)(p)[3]<<24))
#define lpGetNumElements(p) (((uint32_t)(p)[4]<<0) | \
((uint32_t)(p)[5]<<8))
header 后面的,都是其存储的元素或者 EOF(EOF 可能是借用了标准库文件 IO 的那个 EOF 的意思)。lpNew 调用后,结果是这样的。后面留一截空的,表示用户可能预留分配了一定的空间,这个空间可以用 lpNew 的 capacity 指定。
元素
每个元素都按如下规则编码:
enc | backlen | enc | backlen |
---|
enc 就是真正存储元素信息的部分,backlen 是为了方便反向遍历而设置的值。enc 和 backlen 占多少字节是不确定的。他们的编码也不同。redis 中,对 listpack 的元素的任何操作都必须传递该元素的开始指针 p,也就是 enc 开头的那个字节的地址,这样就可以凭借这个指针确定上一个元素的 backlen 的最后一个字节,也就是 p - 1。
那么 enc 是如何编码的?在源代码中可以见到一堆的宏定义:
#define LP_ENCODING_7BIT_UINT 0
#define LP_ENCODING_7BIT_UINT_MASK 0x80
#define LP_ENCODING_IS_7BIT_UINT(byte) (((byte)&LP_ENCODING_7BIT_UINT_MASK)==LP_ENCODING_7BIT_UINT)
#define LP_ENCODING_7BIT_UINT_ENTRY_SIZE 2
#define LP_ENCODING_6BIT_STR 0x80
#define LP_ENCODING_6BIT_STR_MASK 0xC0
...
想了想还是画个表比较好。
类型 | 二进制表示 |
---|---|
6bit 表示字符串长度的 str | 10xxxxxx |
7bit 表示正整数的 uint | 0xxxxxxx |
13bit 表示带符号整数的 int | 110xxxxx xxxxxxxx |
12bit 表示字符串长度的 str | 1110xxxx xxxxxxxx |
16bit 表示带符号整数的 int | 11110001 xxxxxxxx xxxxxxxx |
24bit 表示带符号整数的 int | 11110010 xxxxxxxx xxxxxxxx xxxxxxxx |
32bit 表示带符号整数的 int | 11110011 xxxxxxxx ...... |
64bit 表示带符号整数的 int | 11110100 xxxxxxxx ...... |
32bit 表示字符串长度的 str | 11110000 字符串 |
EOF | 11111111 |
仔细看就会发现,每一个编码的前缀都不一样。这也就是 redis 分辨某一个元素的类型的依据。其中表示 int 类型的部分,会把它变成整数后再存储。原因是补码表示有一定问题。比如 13 bit 带符号整数会这么转换:
} else if (v >= -4096 && v <= 4095) {
/* 13 bit integer. */
if (v < 0) v = ((int64_t)1<<13)+v;
if (intenc != NULL) {
intenc[0] = (v>>8)|LP_ENCODING_13BIT_INT;
intenc[1] = v&0xff;
}
if (enclen != NULL) *enclen = 2;
}
...
接下来就是 backlen 的编码。backlen 是无符号整数,所以不需要考虑负数的问题。它的想法就是把 backlen 分成 7 位一组。
范围 | 结果 |
---|---|
[0,2^7-1) |
0xxxxxxx |
[2^7,2^14-1) |
0xxxxxxx 1xxxxxxx |
[2^14,2^21-1) |
0xxxxxxx 1xxxxxxx 1xxxxxxx |
[2^21,2^28-1) |
0xxxxxxx 1xxxxxxx 1xxxxxxx 1xxxxxxx |
[2^28,2^32-1) |
0xxxxxxx 1xxxxxxx 1xxxxxxx 1xxxxxxx 1xxxxxxx |
最后一行不是 2^35,原因是 listpack 最多也就只能到 2^32 大小。
下面是一个例子(对,string 不以 \0 结尾)
listpack 增删改操作
其实若真理解了 listpack 的编码,这一部分不是很有必要。因为,增无非是计算一下新增元素占多少空间,backlen 占多少空间,调用 memmove 移动一下插入位置以及其后面的元素。删无非是把后面的移动到前面。改也类似。所以 listpack 就把这几个功能做成了一个函数 lpInsert。增删改值得提的就是:
- 把插入到后面转换为一次 next,再插入到前面。
if (where == LP_AFTER) {
p = lpSkip(p);
where = LP_BEFORE;
ASSERT_INTEGRITY(lp, p); // do-while 的写法
}
- realloc 之后一定要更新 offset
if ((lp = lp_realloc(lp,new_listpack_bytes)) == NULL) return NULL;
dst = lp + poff;
- 如果一个字符串表示为一个整数,那么 listpack 会把它存储为整数
static inline int lpEncodeGetType(unsigned char *ele, uint32_t size, unsigned char *intenc, uint64_t *enclen) {
int64_t v;
if (lpStringToInt64((const char*)ele, size, &v)) {
lpEncodeIntegerGetType(v, intenc, enclen);
return LP_ENCODING_INT;
} else {
if (size < 64) *enclen = 1+size;
else if (size < 4096) *enclen = 2+size;
else *enclen = 5+(uint64_t)size;
return LP_ENCODING_STRING;
}
}
-
批量插入,批量删除,范围删除也无非是把多次 memmove 操作变成一次移动罢了。
-
listpack 中共用户使用的结构体。它在批量操作、随机获取元素中会用。
/* Each entry in the listpack is either a string or an integer. */
typedef struct {
/* When string is used, it is provided with the length (slen). */
unsigned char *sval;
uint32_t slen;
/* When integer is used, 'sval' is NULL, and lval holds the value. */
long long lval;
} listpackEntry;
- lpGetWithSize 的参数含义。这个函数也是从 listpack 中获得值的方法。除此之外里面还有一个方便检查错误的技巧。
static inline unsigned char *lpGetWithSize(unsigned char *p, int64_t *count,
unsigned char *intbuf, uint64_t *entry_size);
// 如果 p 是字符串,那么
// 1. 返回 p 这个字符串的指针
// 2. *count 设置为该字符串长度
// 3. *entry_size 设置为 p 这个节点的总空间占用
// 如果 p 是整数,
// 如果 intbuf 是 NULL,那么
// 1. *count 设置为该整数
// 2. 返回 NULL
// 否则
// 1. 将该整数用字符串表示到 intbuf
// 2. 返回 intbuf
...
} else {
uval = 12345678900000000ULL + p[0];
negstart = UINT64_MAX;
negmax = 0;
}
...
- 获取 enc 的大小
redis 给了两个版本,一个是 Unsafe,它不仅仅会访问 p 的第一个字节,后续的一部分也会被访问到,用于获得整个编码结果的长度。另一个版本则只访问 p 的第一个字节,只能确定类型,以及整数类型的编码长度。之所以一个字节就够了,是因为前面提到的编码,只要确定第一个字节就可以确定该元素存储的值的类型了。
static inline uint32_t lpCurrentEncodedSizeUnsafe(unsigned char *p);
static inline uint32_t lpCurrentEncodedSizeBytes(unsigned char *p);
listpack 查找元素
由于 listpack 会把可以转换为整数的字符串直接存储为整数,所以查找的过程并不是简单的字符串匹配。
它首先还是遍历,遍历的时候进行比较。比较函数稍微有点意思。
/* Comparator function to find item */
static inline int lpFindCmp(const unsigned char *lp, unsigned char *p,
void *user, unsigned char *s, long long slen) {
struct lpFindArg *arg = user;
if (s) {
if (slen == arg->slen && memcmp(arg->s, s, slen) == 0) {
return 0;
}
} else {
if (arg->vencoding == 0) {
if (arg->slen >= 32 || arg->slen == 0 || !lpStringToInt64((const char*)arg->s, arg->slen, &arg->vll)) {
arg->vencoding = UCHAR_MAX;
} else {
arg->vencoding = 1;
}
}
if (arg->vencoding != UCHAR_MAX && slen == arg->vll) {
return 0;
}
}
...
s 如果是 NULL,把 slen 当作整数看待,否则把它当作字符串 s 的长度看待。因为 listpack 可能会把字符串转换为 int,所以这里的 arg->vll 相当于一个结果缓存,而 arg->vencoding 保证改转换只发生一次。调用它的函数是 lpFindCb,它被 lpFind 调用。
unsigned char *lpFind(unsigned char *lp, unsigned char *p, unsigned char *s,
uint32_t slen, unsigned int skip)
{
struct lpFindArg arg = {
.s = s,
.slen = slen
};
return lpFindCb(lp, p, &arg, lpFindCmp, skip);
}
lpFind 的参数单纯表示字符串,所以 lpFindCmp 有字符串转 int。
listpack 随机获取值
随机获取值的流程是,先获取一个随机下标,再掉 lpSeek。所以这个效率比较低。
void lpRandomPair(unsigned char *lp, unsigned long total_count,
listpackEntry *key, listpackEntry *val, int tuple_len)
{
unsigned char *p;
assert(tuple_len >= 2);
/* Avoid div by zero on corrupt listpack */
assert(total_count);
int r = (rand() % total_count) * tuple_len;
assert((p = lpSeek(lp, r)));
key->sval = lpGetValue(p, &(key->slen), &(key->lval));
if (!val)
return;
assert((p = lpNext(lp, p)));
val->sval = lpGetValue(p, &(val->slen), &(val->lval));
}