文章目录
概述
在Redis3.2之后,统一用quicklist来存储列表对象,quicklist存储了一个双向列表,每个列表的节点是一个ziplist,所以实际上quicklist就是linkedlist和ziplist的结合。
新老版本完整对比参见《Redis对象redisObject之列表对象》
为什么引入quicklist
我们先看看之前的版本,单独采用linkedlist或ziplist时的优缺点:
linkedlist优缺点
- 优点: 插入,删除节点复杂度很低,简单
- 缺点: 除保存数据外还需要保存prev, next两个指针,内存利用率低,双向链表的各个节点是单独的内存块,地址不连续,节点多了容易产生内存碎片。
ziplist优缺点
- 优点: 存储在一段连续的内存上,不容易产生内存碎片,内存利用率高。
- 缺点: 插入和删除操作需要频繁的申请和释放内存, 同时会发生内存拷贝,数据量大时内存拷贝开销较大。
基于上述问题,quicklist的设计是一个空间和时间的折中, quicklist结合了双向链表和ziplist的优点,核心思想是达到永远使用ziplist的特性,并利用linkedlist结构来减少ziplist的长度。
假设ziplist采用默认值list-max-ziplist-size=-2 ,表示ziplist的总大小为8k(3.2版本之前默认大小是64k,大大减少了ziplist的长度),超过该长度总和,就会新建一个链表节点,假设有1000个数据,每个数据0.5k,ziplist~=16,那么我们则有约55链表节点:
- 总是使用到 ziplist,减少内存碎片。老版本当长度超标后,ziplist就会转为链表,代价就是容易出现内存碎片。
- 大大减少了单个ziplist长度,当发生修改时,修改一个8k总大小的ziplist成本远小于64K的总大小的ziplist。根据ziplist特性,数据量大时内存拷贝开销较大,因此,当长度比较少时,效率就越高
1. quicklist内部存储结构
quicklist中每一个节点都是一个quicklistNode对象,其数据结构定义为:
typedef struct quicklistNode {
struct quicklistNode *prev;//前一个节点
struct quicklistNode *next;//后一个节点
unsigned char *zl;//当前指向的ziplist或者quicklistLZF
unsigned int sz;//当前ziplist占用字节
unsigned int count : 16;//ziplist中存储的元素个数,16字节(最大65535个)
unsigned int encoding : 2; //是否采用了LZF压缩算法压缩节点 1:RAW 2:LZF
unsigned int container : 2; //存储结构,NONE=1, ZIPLIST=2
unsigned int recompress : 1; //当前ziplist是否需要再次压缩(如果前面被解压过则为true,表示需要再次被压缩)
unsigned int attempted_compress : 1;//测试用
unsigned int extra : 10; //后期留用
} quicklistNode;
然后各个quicklistNode就构成了一个列表quicklist:
typedef struct quicklist {
quicklistNode *head;//列表头节点
quicklistNode *tail;//列表尾节点
unsigned long count;//ziplist中一共存储了多少元素,即:每一个quicklistNode内的count相加
unsigned long len; //双向链表的长度,即quicklistNode的数量
int fill : 16;//填充因子
unsigned int compress : 16;//压缩深度 0-不压缩
} quicklist;
quicklist封装quicklistNode作用类似链表结构中list 对listNode的封装,提供收尾节点的指针,方便双向遍历。
根据这两个结构,我们可以得到Redis3.2之后的列表对象的一个简图:
1.1 控制ziplist元素长度
quicklist结合了双向链表和ziplist的优点,但是同样也存在一个问题,一个quicklist包含多长的ziplist合适呢?需要找到一个平衡点:
- ziplist太短,内存碎片越多。
- ziplist太长,分配大块连续内存空间的难度就越大。
如何保持ziplist的合理长度,取决于具体的应用场景。
我们可以通过list-max-ziplist-size
参数来控制ziplist的长度,基于2种原则,即元素的长度或元素大小的总和。
语法例如:
list-max-ziplist-size -2 '默认值'
-
当取正值的时候,表示按照数据项个数来限定每个quicklist节点上的ziplist长度。比如,当这个参数配置成5的时候,表示每个quicklist节点的ziplist最多包含5个数据项。
-
当取负值的时候,表示按照占用字节数来限定每个quicklist节点上的ziplist长度。这时,它只能取-1到-5这五个值,每个值含义如下:
-5: 每个quicklist节点上的ziplist大小不能超过64 Kb。(注:1kb => 1024 bytes) -4: 每个quicklist节点上的ziplist大小不能超过32 Kb。 -3: 每个quicklist节点上的ziplist大小不能超过16 Kb。 -2: 每个quicklist节点上的ziplist大小不能超过8 Kb。(-2是Redis给出的默认值) -1: 每个quicklist节点上的ziplist大小不能超过4 Kb。
1.2 quicklist的compress属性
list设计最容易被访问的是列表两端的数据,中间的访问频率很低,如果符合这个场景,list还有一个配置,可以对中间节点进行压缩(采用的LZF——一种无损压缩算法),进一步节省内存。配置如下:
list-compress-depth 0
compress属性表示压缩深度,这个参数表示一个quicklist两端不被压缩的节点个数。
注:这里的节点个数是指quicklist双向链表的节点个数,而不是指ziplist里面的数据项个数。实际上,一个quicklist节点上的ziplist,如果被压缩,就是整体被压缩的。
可以通过参数list-compress-depth控制:
- 0:不压缩(
默认值
) - 1:首尾第1个元素不压缩
- 2:首位前2个元素不压缩
- 3:首尾前3个元素不压缩
- 以此类推
1.3 quicklistNode的zl指针
zl指针默认指向了ziplist,sz属性记录了当前ziplist占用的字节,不过这仅仅限于当前节点没有被压缩(LZF压缩算法)的情况,如果当前节点被压缩了,那么zl指针会指向另一个对象quicklistLZF,quicklistLZF是一个4+N字节的结构:
typedef struct quicklistLZF {
unsigned int sz;// LZF大小
char compressed[];//被压缩的内容
} quicklistLZF;
参考:
《【Redis系列3】Redis列表对象之linkedlist(双端列表)和ziplist(压缩列表)及quicklick(快速列表)实现原理分析》