链表和列表不是一个概念,之后会多次看到这两个词,不要搞混。
Redis 在后来的版本新增设计了两种数据结构:quicklist(Redis 3.2 引入)和 listpack(Redis 5.0 引入)。这两种数据结构的设计目标,就是尽可能地保持压缩列表节省内存的优势,同时解决压缩列表的连锁更新的问题。
这一篇来讲 quicklist 的实现。
quicklist 和压缩列表的区别
其实 quicklist 就是双向链表里存压缩列表,一个 quicklist 就是一个链表,而链表中的每个元素又是一个压缩列表。
quicklist 解决连锁更新的办法:不解决。
quicklist 通过控制每个链表节点中的压缩列表的大小或者元素个数,来规避连锁更新的问题。因为压缩列表元素越少或越小,连锁更新带来的影响就越小,从而提供了更好的访问性能。
结构体定义
quicklist 的结构体跟链表的结构体类似,都包含了表头和表尾,区别在于 quicklist 的节点是 quicklistNode。
c
typedef struct quicklist {
// quicklist 的链表头节点
quicklistNode *head;
// quicklist 的链表尾节点
quicklistNode *tail;
// 所有压缩列表中的总元素个数
unsigned long count;
// quicklistNode 的个数
unsigned long len;
...
} quicklist;
接下来看 quicklistNode 的结构定义:
c
typedef struct quicklistNode {
// 前一个 quicklistNode
struct quicklistNode *prev;
// 后一个 quicklistNode
struct quicklistNode *next;
// quicklistNode 指向的压缩列表
unsigned char *zl;
// 压缩列表的的字节大小
unsigned int sz;
// 压缩列表的元素个数
unsigned int count : 16;
...
} quicklistNode;
可以看到,quicklistNode 结构体里包含了前一个节点和下一个节点指针,这样每个 quicklistNode 形成了一个双向链表。但是链表节点的元素不再是单纯保存元素值,而是保存了一个压缩列表,quicklistNode 结构体里有个指向压缩列表的指针 *zl
。
按惯例这里来张图更形象:
在向 quicklist 添加一个元素的时候,不会像普通的链表那样,直接新建一个链表节点。而是会检查插入位置的压缩列表是否能容纳该元素,如果能容纳就直接保存到 quicklistNode 结构里的压缩列表,如果不能容纳,才会新建一个新的 quicklistNode 结构。
简单来说插入是这样的,但是实际使用的时候不会这么简单,因为肯定会有人觉得不能指定插入位置的插入太无聊了,假如想在原来的 quicklist 中间某个节点的压缩列表里的某个位置开始插入,或者某个位置之前插入,问题就有些复杂了。
插入位置的压缩列表还有足够的空位留给插入的数据,那就万事大吉直接放进去。
插入位置的压缩列表空间不够,插入的位置还是这个压缩列表的两端(从压缩列表的头往前插入,或者从压缩列表的尾往后插入),并且压缩列表所在 quicklistNode 两端相邻的 quicklistNode 的压缩列表有足够空位,就插入到这里。
插入位置的压缩列表空间不够,插入位置还是压缩列表的两端,并且压缩列表所在 quicklistNode 两端相邻的 quicklistNode 的压缩列表空间也不够,就只能新建一个 quicklistNode 带上空的压缩列表给这个新插入的值用。
其他情况(主要是插入位置是压缩列表中间),这种就要把原来的压缩列表从插入位置断开,再新建一个 quicklistNode 承载多出来的压缩列表,再往对应位置上插入。
我说的这些情况似乎不包括一个数据就把空的压缩列表撑爆的情况,这种情况要不然就要提前分割数据要不然就不会用到这个数据结构来存。
增删改查的增说完了,删改查简单,这个结构支持的查是根据索引查对应位置上的东西,删的话有可能删区间,区间有可能跨节点,这个因为有压缩列表的大小,也是能定位到从哪到哪都不要直接删的。
quicklist 会控制 quicklistNode 结构里的压缩列表的大小或者元素个数,来规避潜在的连锁更新的风险,但是这并没有完全解决连锁更新的问题。