为了加快速度,Redis都作了哪些“变态”设计

前言

列表对象是 Redis5 种基础数据类型之一,在 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 是一个双向列表,每一个节点都会存储指向上一个节点和指向下一个节点的指针。linkedlist 由于每一个节点之间的空间是不连续的,因此可能会形成过多的内存空间碎片。node

linkedlist存储结构

链表中每个节点都是一个 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 节点为 NULLtail 节点的 next 节点也为 NULL,因此从头节点开始遍历,当发现 tailNULL 时,则能够认为已经到了列表末尾。数据结构

当咱们设置一个列表对象时,在 Redis 3.2 版本以前咱们能够获得以下存储示意图:函数

ziplist

压缩列表在前面已经介绍过,想要详细了解的能够点击这里性能

linkedlist 和 ziplist 的选择

Redis3.2 以前,linkedlistziplist 两种编码能够进选择切换,若是须要列表使用 ziplist 编码进行存储,则必须知足如下两个条件:测试

  • 列表对象保存的全部字符串元素的长度都小于 64 字节。
  • 列表对象保存的元素数量小于 512 个。

一旦不知足这两个条件的任意一个,则会使用 linkedlist 编码进行存储。ui

PS:这两个条件能够经过参数 list-max-ziplist-valuelist-max-ziplist-entries 进行修改。

这两种列表能在特定的场景下发挥各自的做用,应该来讲已经能知足大部分需求了,而后 Redis 并不知足于此,因而一场改革引起了,quicklist 横空出世。

quicklist

Redis 3.2 版本以后,为了进一步提高 Redis 的性能,列表对象统一采用 quicklist 来存储列表对象。quicklist存储了一个双向列表,每一个列表的节点是一个 ziplist,因此实际上 quicklist 并非一个新的数据结构,它就是linkedlistziplist 的结合,而后被命名为快速列表。

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;

根据这两个结构,咱们能够获得 Redis 3.2 版本以后的列表对象的一个存储结构示意图:

quicklist 的 compress 属性

compress 是用来表示压缩深度,ziplist 除了内存空间是连续以外,还能够采用特定的 LZF 压缩算法来将节点进行压缩存储,从而更进一步的节省空间,压缩深度能够经过参数 list-compress-depth 控制:

  • 0:不压缩(默认值)
  • 1:首尾第1个元素不压缩
  • 2:首位前2个元素不压缩
  • 3:首尾前3个元素不压缩
  • 以此类推

注意:之因此采起这种压缩两端节点的方式是由于不少场景都是两端的元素访问率最高的,而中间元素访问率相对较低,因此在实际使用时,咱们能够根据本身的实际状况选择是否进行压缩,以及具体的压缩深度。

quicklistNode 的 zl 指针

zl 指针默认指向了 ziplist,上面提到 quicklistNode 中有一个 sz 属性记录了当前 ziplist 占用的字节,不过这仅仅限于当前节点没有被压缩(经过LZF 压缩算法)的状况,若是当前节点被压缩了,那么被压缩节点的 zl 指针会指向另外一个对象 quicklistLZF,而不会直接指向 ziplistquicklistLZF 是一个 4+N 字节的结构:

typedef struct quicklistLZF {
    unsigned int sz;// LZF大小,占用4字节
    char compressed[];//被压缩的内容,占用N字节
} quicklistLZF;

quicklist 对比原始两种编码的改进

quicklist 一样采用了 linkedlist 的双端列表特性,而后 quicklist 中的每一个节点又是一个 ziplist,因此quicklist 就是综合平衡考虑了 linkedlist 容易产生空间碎片的问题和 ziplist 的读写性能两个维度而设计出来的一种数据结构。使用 quicklist 须要注意如下 2 点:

  • 若是 ziplist 中的 entry 个数过少,最极端状况就是只有 1entry 的压缩列表,那么此时 quicklist 就至关于退化成了一个普通的 linkedlist
  • 若是 ziplist 中的 entry 过多,那么也会致使一次性须要申请的内存空间过大(ziplist 空间是连续的),并且由于 ziplist 自己的就是以时间换空间,因此会过多 entry 也会影响到列表对象的读写性能。

ziplist 中的 entry 个数能够经过参数 list-max-ziplist-size 来控制:

list-max-ziplist-size 1

注意:这个参数能够配置正数也能够配置负数。正数表示限制每一个节点中的 entry 数量,若是是负数则只能为 -1~-5,其表明的含义以下:

  • -1:每一个 ziplist 最多只能为 4KB
  • -2:每一个 ziplist 最多只能为 8KB
  • -3:每一个 ziplist 最多只能为 16KB
  • -4:每一个 ziplist 最多只能为 32KB
  • -5:每一个 ziplist 最多只能为 64KB

列表对象经常使用操做命令

  • lpush key value1 value2:将一个或者多个 value 插入到列表 key 的头部,key 不存在则建立 keyvalue2value1 以后)。
  • lpushx key value1 value2:将一个或者多个 value 插入到列表 key 的头部,key 不存在则不作任何处理(value2value1 以后)。
  • lpop key:移除并返回 key 值的列表头元素。
  • rpush key value1 value2:将一个或者多个 value 插入到列表 key 的尾部,key 不存在则建立 keyvalue2value1 以后)。
  • rpushx key value1 vaue2:将一个或者多个 value 插入到列表 key 的尾部,key 不存在则不作任何处理(value2value1 以后)。
  • rpop key:移除并返回列表 key 的尾元素。
  • llen key:返回列表 key 的长度。
  • lindex key index:返回列表 key 中下标为 index 的元素。index 为正数(从 0 开始)表示从队头开始算,index 为负数(从-1开始)则表示从队尾开始算。
  • lrange key start stop:返回列表 key 中下标 [start,end] 之间的元素。
  • lset key index value:将 value 设置到列表 key 中指定 index 位置,key 不存在或者 index 超出范围则会报错。
  • ltrim key start end:截取列表中 [start,end] 之间的元素,并替换原列表保存。

了解了操做列表对象的经常使用命令,咱们就能够来验证下前面提到的列表对象的类型和编码了,在测试以前为了防止其余 key 值的干扰,咱们先执行 flushall 命令清空 Redis 数据库。

接下来依次输入命令:

  • lpush name zhangsan
  • type name
  • object encoding name

能够看到,经过 type 命令输出的是 list,说明当前 name 存的是一个列表对象,而且编码是 quicklist(示例中用的是 5.0.5 版本)。

总结

本文主要介绍了 Redis5 种经常使用数据类型中的 列表对象,并介绍了底层的存储结构 quicklist,并分别对旧版本的两种底层数据 linkedlistziplist 进行了分析对比得出了为何 Redis 最终要采用 quicklist 来存储列表对象。

相关文章
相关标签/搜索