这篇博文用来解读 Redis 数据类型 List 的一种实现。数据结构 quicklist。虽然 List 类型有多种实现,但 quicklist 是最常用的。
quicklist 是一个双向链表,但同时也是一个复合的结构体。结构中包括了另一种结构 ziplist。如果不熟悉这种结构体,请先阅读这篇博文:《ziplist - 压缩列表》。
文中介绍了 ziplist 是一种十分节省内存的结构,紧凑的内存布局、变长的编码方式在内存使用量上有着不小的优势。但是修改操作下并不能像一般的链表那么容易,需要从新分配新的内存,然后复制到新的空间。
所以结合了 ziplist 节省内存和双向链表优点的 quicklist 产生了。
在 quicklist.c 中可以找到名为 quicklist 的结构体:
typedef struct quicklist {
quicklistNode *head;
quicklistNode *tail;
unsigned long count;
unsigned long len;
int fill : QL_FILL_BITS;
unsigned int compress : QL_COMP_BITS;
unsigned int bookmark_count: QL_BM_BITS;
quicklistBookmark bookmarks[];
} quicklist;
这个结构体定义了 quicklist 的布局,在 64 位的操作系统中它使用了 40 字节。看起来不是很复杂。结构中的各项代表含义如下:
接下来继续解读用来描述 ziplist 的 quicklistNode 结构,同样在在 quicklist.c 中可以找到名为 quicklistNode 的结构体:
typedef struct quicklistNode {
struct quicklistNode *prev;
struct quicklistNode *next;
unsigned char *zl;
unsigned int sz;
unsigned int count : 16;
unsigned int encoding : 2;
unsigned int container : 2;
unsigned int recompress : 1;
unsigned int attempted_compress : 1;
unsigned int extra : 10;
} quicklistNode;
Redis 会将 quicklistNode 控制在 32 个字节大小,其中每项的定义如下:
上面 quicklistNode 若是被压缩则会使用 quicklistLZF 机构,它的布局是比较简单的:
typedef struct quicklistLZF {
unsigned int sz; /* LZF size in bytes*/
char compressed[];
} quicklistLZF;
代码中注释:quicklistLZF is a 4+N byte struct holding ‘sz’ followed by ‘compressed’。
首先我们思考一个问题,一个数据结构的好坏大致会受空间、时间复杂度的影响。quicklis 的出现也正是考虑到这点,想在俩者之间找到一个最佳的平衡点。所以它使用了双向链表加 ziplist 的复合组合。
第一 双向链表能在头尾在 O(1)下找到一个元素,若是在中部查找则平均复杂度也只是O(N),N 是 entry 的个数。
第二 ziplist 使用了紧凑布局和可变编码方式大大降低了内存的使用。这就是 quicklis 作为 List 首选的实现方案。
但是若是 quicklistNode 中 ziplist 的 entry 的个数设置的不恰当,那 quicklist 的性能也会大幅降低。比如下面的情况:
所以在实际使用中,要根据数据特点设置一个比较合理的值。
list-max-ziplist-size 取值范围可以是正数也可以是负数,不能为 0 。当取正数时候代表每个 ziplist 可以存储的最大 entry 数。负数时候去值范围只能是 -5~-1 ,各自代表 ziplist 最大字节大小。
布局图:
对于 List 来说:最重要、常用的操作无非就是 push、pop、len等。在 quicklist 中定义了这些操作的函数接口。
当往 List 中 push 元素时候,若 list 不存在 。会先创建一个空的 quicklist , 初始化各项。代码比较清晰,看一下即可明白。
quicklist *quicklistCreate(void) {
// quicklist 结构体
struct quicklist *quicklist;
// 内存分配,该函数具体逻辑可以看 zmalloc.c 文件
quicklist = zmalloc(sizeof(*quicklist));
// 下面都是初始化赋值
quicklist->head = quicklist->tail = NULL;
quicklist->len = 0;
quicklist->count = 0;
quicklist->compress = 0;
quicklist->fill = -2;
quicklist->bookmark_count = 0;
return quicklist;
}
若 list 存在,则调用 quicklistPush 函数进行 push 操作。
void quicklistPush(quicklist *quicklist, void *value, const size_t sz, int where) {
// 判断 push 方向 调用实际的 push 函数。
// 头部
if (where == QUICKLIST_HEAD) {
quicklistPushHead(quicklist, value, sz);
} else if (where == QUICKLIST_TAIL) {
// 尾部
quicklistPushTail(quicklist, value, sz);
}
}
实际上 quicklistPushHead 和 quicklistPushTail 函数的逻辑是一样的,这里就只用 quicklistPushHead 进行解析。
int quicklistPushHead(quicklist *quicklist, void *value, size_t sz) {
quicklistNode *orig_head = quicklist->head;
// 判断节点上的 ziplist 是否操作配置设置的大小, 若没有超过 _quicklistNodeAllowInsert 函数返回 1。
if (likely(_quicklistNodeAllowInsert(quicklist->head, quicklist->fill, sz))) {
// 调用 ziplistPush 往 ziplist 中添加一个 entry
quicklist->head->zl = ziplistPush(quicklist->head->zl, value, sz, ZIPLIST_HEAD);
// 更新记录 ziplist 总字节数
quicklistNodeUpdateSz(quicklist->head);
} else {
// 创建一个新的 节点
quicklistNode *node = quicklistCreateNode();
// 调用 ziplistPush 往 ziplist 中添加一个 entry
node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_HEAD);
// 更新记录 ziplist 总字节数
quicklistNodeUpdateSz(node);
// 实际上是调用 __quicklistInsertNode 根据 after 的值,判断 new_node 插入位置。相对于 old_node。
_quicklistInsertNodeBefore(quicklist, quicklist->head, node);
}
// ziplist 的 entry 个数加一
quicklist->count++;
quicklist->head->count++;
return (orig_head != quicklist->head);
}
解读完 push 操作后,接着解读 pop。pop 是调用函数 quicklistPop 。
int quicklistPop(quicklist *quicklist, int where, unsigned char **data,unsigned int *sz, long long *slong) {
unsigned char *vstr;
unsigned int vlen;
long long vlong;
// 若当前 entry 为空,则直接返回 0;
if (quicklist->count == 0)
return 0;
// 实际逻辑是调用 quicklistPopCustom 、在调用 ziplistGet 获取、 最后调用 quicklistDelIndex 删掉元素。
// 具体的逻辑自行查阅代码
int ret = quicklistPopCustom(quicklist, where, &vstr, &vlen, &vlong, _quicklistSaver);
if (data)
*data = vstr;
if (slong)
*slong = vlong;
if (sz)
*sz = vlen;
return ret;
}
得益于 quicklist 结构的友好设计,其实就是空间换时间。获取 list 的长度,是不需要遍历节点中的 ziplist 的个数。 这种设计在 redis 的各种数据结构中是十分常见的,我们在实际开发中其实也可以采纳这种设计思想。
unsigned long quicklistCount(const quicklist *ql) {
return ql->count;
}
对操作接口的解读就到这里了,感兴趣的可以直接去 quicklist.c 中查阅。