列表对象是 Redis
中 5
种基础数据类型之一,在 Redis 3.2
版本以前,列表对象底层存储结构有两种:linkedlist
(双端列表)和 ziplist
(压缩列表),而在 Redis 3.2
版本以后,列表对象底层存储结构只有一种:quicklist
(快速列表),难道经过精心设计的 ziplist
最终被 Redis
抛弃了吗?html
同字符串对象同样,列表对象到底使用哪种数据结构来进行存储也是经过编码来进行区分:java
编码属性 | 描述 | object encoding命令返回值 |
---|---|---|
OBJ_ENCODING_LINKEDLIST | 使用 linkedlist 实现列表对象 |
linkedlist |
OBJ_ENCODING_ZIPLIST | 使用 ziplist 实现列表对象 |
ziplist |
OBJ_ENCODING_QUICKLIST | 使用 quicklist 实现列表对象 |
quicklist |
linkedlist
是一个双向列表,每一个节点都会存储指向上一个节点和指向下一个节点的指针。linkedlist
由于每一个节点之间的空间是不连续的,因此可能会形成过多的内存空间碎片。node
链表中每个节点都是一个 listNode
对象(源码 adlist.h
内),不过须要注意的是,列表中的 value
其实也是一个字符串对象,其余几种数据类型其内部最终也是会嵌套字符串对象,字符串对象也是惟一一种会被其余对象引用的基本类型:算法
typedef struct listNode { struct listNode *prev;//前一个节点 struct listNode *next;//后一个节点 void *value;//值(字符串对象) } listNode;
而后会将其再进行封装成为一个 list
对象(源码 adlist.h
内):数据库
typedef struct list { listNode *head;//头节点 listNode *tail;//尾节点 void *(*dup)(void *ptr);//节点值复制函数 void (*free)(void *ptr);//节点值释放函数 int (*match)(void *ptr, void *key);//节点值对比函数 unsigned long len;//节点数量 } list;
Redis
中对 linkedlist
的访问是以 NULL
值为终点的,由于 head
节点的 prev
节点为 NULL
,tail
节点的 next
节点也为 NULL
,因此从头节点开始遍历,当发现 tail
为 NULL
时,则能够认为已经到了列表末尾。数据结构
当咱们设置一个列表对象时,在 Redis 3.2
版本以前咱们能够获得以下存储示意图:函数
压缩列表在前面已经介绍过,想要详细了解的能够点击这里。性能
在 Redis3.2
以前,linkedlist
和 ziplist
两种编码能够进选择切换,若是须要列表使用 ziplist
编码进行存储,则必须知足如下两个条件:测试
64
字节。512
个。一旦不知足这两个条件的任意一个,则会使用 linkedlist
编码进行存储。ui
PS:这两个条件能够经过参数 list-max-ziplist-value
和 list-max-ziplist-entries
进行修改。
这两种列表能在特定的场景下发挥各自的做用,应该来讲已经能知足大部分需求了,而后 Redis
并不知足于此,因而一场改革引起了,quicklist
横空出世。
在 Redis 3.2
版本以后,为了进一步提高 Redis
的性能,列表对象统一采用 quicklist
来存储列表对象。quicklist
存储了一个双向列表,每一个列表的节点是一个 ziplist
,因此实际上 quicklist
并非一个新的数据结构,它就是linkedlist
和 ziplist
的结合,而后被命名为快速列表。
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;
根据这两个结构,咱们能够获得 Redis 3.2
版本以后的列表对象的一个存储结构示意图:
compress
是用来表示压缩深度,ziplist
除了内存空间是连续以外,还能够采用特定的 LZF
压缩算法来将节点进行压缩存储,从而更进一步的节省空间,压缩深度能够经过参数 list-compress-depth
控制:
注意:之因此采起这种压缩两端节点的方式是由于不少场景都是两端的元素访问率最高的,而中间元素访问率相对较低,因此在实际使用时,咱们能够根据本身的实际状况选择是否进行压缩,以及具体的压缩深度。
zl
指针默认指向了 ziplist
,上面提到 quicklistNode
中有一个 sz
属性记录了当前 ziplist
占用的字节,不过这仅仅限于当前节点没有被压缩(经过LZF
压缩算法)的状况,若是当前节点被压缩了,那么被压缩节点的 zl
指针会指向另外一个对象 quicklistLZF
,而不会直接指向 ziplist
。quicklistLZF
是一个 4+N
字节的结构:
typedef struct quicklistLZF { unsigned int sz;// LZF大小,占用4字节 char compressed[];//被压缩的内容,占用N字节 } quicklistLZF;
quicklist
一样采用了 linkedlist
的双端列表特性,而后 quicklist
中的每一个节点又是一个 ziplist
,因此quicklist
就是综合平衡考虑了 linkedlist
容易产生空间碎片的问题和 ziplist
的读写性能两个维度而设计出来的一种数据结构。使用 quicklist
须要注意如下 2
点:
ziplist
中的 entry
个数过少,最极端状况就是只有 1
个 entry
的压缩列表,那么此时 quicklist
就至关于退化成了一个普通的 linkedlist
。ziplist
中的 entry
过多,那么也会致使一次性须要申请的内存空间过大(ziplist
空间是连续的),并且由于 ziplist
自己的就是以时间换空间,因此会过多 entry
也会影响到列表对象的读写性能。ziplist
中的 entry
个数能够经过参数 list-max-ziplist-size
来控制:
list-max-ziplist-size 1
注意:这个参数能够配置正数也能够配置负数。正数表示限制每一个节点中的 entry
数量,若是是负数则只能为 -1~-5
,其表明的含义以下:
ziplist
最多只能为 4KB
ziplist
最多只能为 8KB
ziplist
最多只能为 16KB
ziplist
最多只能为 32KB
ziplist
最多只能为 64KB
value
插入到列表 key
的头部,key
不存在则建立 key
(value2
在value1
以后)。value
插入到列表 key
的头部,key
不存在则不作任何处理(value2
在value1
以后)。key
值的列表头元素。value
插入到列表 key
的尾部,key
不存在则建立 key
(value2
在value1
以后)。value
插入到列表 key
的尾部,key
不存在则不作任何处理(value2
在value1
以后)。key
的尾元素。key
的长度。key
中下标为 index
的元素。index
为正数(从 0
开始)表示从队头开始算,index
为负数(从-1开始)则表示从队尾开始算。key
中下标 [start,end]
之间的元素。value
设置到列表 key
中指定 index
位置,key
不存在或者 index
超出范围则会报错。[start,end]
之间的元素,并替换原列表保存。了解了操做列表对象的经常使用命令,咱们就能够来验证下前面提到的列表对象的类型和编码了,在测试以前为了防止其余 key
值的干扰,咱们先执行 flushall
命令清空 Redis
数据库。
接下来依次输入命令:
lpush name zhangsan
type name
object encoding name
能够看到,经过 type
命令输出的是 list
,说明当前 name
存的是一个列表对象,而且编码是 quicklist
(示例中用的是 5.0.5
版本)。
本文主要介绍了 Redis
中 5
种经常使用数据类型中的 列表对象,并介绍了底层的存储结构 quicklist
,并分别对旧版本的两种底层数据 linkedlist
和 ziplist
进行了分析对比得出了为何 Redis
最终要采用 quicklist
来存储列表对象。