Redis 数据结构——quicklist

一、quicklist 介绍

1、quicklist 简介

Redis 中的 list 数据类型在版本 3.2 之前,其底层的编码是 ziplist 和 linkedlist 实现的,但是在版本 3.2 之后,重新引入了一个 quicklist 的数据结构,list 底层都由 quicklist 实现。

在早期的设计中, 当 list 对象中元素的长度比较小或者数量比较少的时候,采用 ziplist 来存储,当 list 对象中元素的长度比较大或者数量比较多的时候,则会转而使用双向列表 linkedlist 来存储。

快速列表 quicklist 可以看成是用双向链表将若干小型的 ziplist 连接到一起组成的一种数据结构。

说白了就是把 ziplist 和 linkedlist 结合起来。每个双链表节点中保存一个 ziplist,然后每个 ziplist 中存一批list 中的数据(具体 ziplist 大小可配置),这样既可以避免大量链表指针带来的内存消耗,也可以避免 ziplist 更新导致的大量性能损耗,将大的 ziplist 化整为零。

2、ziplist 和 linkedlist 的优缺点

双端链表 linkedlist 便于在表的两端进行 push 和 pop 操作,在插入节点上复杂度很低,但是它的内存开销比较大。首先,它在每个节点上除了要保存数据之外,还要额外保存两个指针;其次,双向链表的各个节点是单独的内存块,地址不连续,节点多了容易产生内存碎片。

压缩列表 ziplist 存储在一段连续的内存上,所以存储效率很高。但是,它不利于修改操作,插入和删除操作需要频繁的申请和释放内存。特别是当 ziplist 长度很长的时候,一次 realloc 可能会导致大批量的数据拷贝。

因此,在 Redis3.2 以后,采用了 quicklist,quicklist 是综合考虑了时间效率与空间效率而引入的新型数据结构。

3、quicklist 极端情况

情况一: 当 ziplist 节点过多的时候,quicklist 就会退化为双向链表。效率较差;效率最差时,一个 ziplist 中只包含一个 entry,即只有一个元素的双向链表。(增加了查询的时间复杂度)

情况二:当 ziplist 元素个数过少时,quicklist 就会退化成为 ziplist,最极端的时候,就是 quicklist 中只有一个 ziplist 节点。(当增加数据时,ziplist 需要重新分配空间)

所以说:quicklist 其实就是综合考虑了时间和空间效率引入的新型数据结构。(使用 ziplist 能提高空间的使用率,使用 linkedlist 能够降低插入元素时的时间)

二、quicklist 结构

image.png
大致介绍下上图中不同的节点,所有 Redis 中 list 其实都是 quicklist,所以像 pop 和 push 等命令中的 list 参数都是 quicklist。quicklist 各字段及其含义如下:

1、quicklist 定义

image.png
quicklist 字段描述:
head:头结点

tail:尾结点

count:在所有的 ziplist 中的 entry 总数

len:quicklistNode 节点总数

fill:每个 quicklist 节点的最大容量

compress:quicklist 的压缩深度,0 表示所有节点都不压缩,否则就表示从两端开始有多少个节点不压缩

bookmark_count:bookmarks 数组的大小

bookmarks:是一个可选字段,用来 quicklist 重新分配内存空间时使用,不使用时不占用空间

为什么不全部节点都压缩,而是流出 compress 这个可配置的口子呢?
其实从统计来看,list 两端的数据变更最为频繁,像 lpush,rpush,lpop,rpop 等命令都是在两端操作,如果频繁压缩或解压缩会代码不必要的性能损耗。

这里还有个 fill 字段,它的含义是每个 quicknode 节点的最大容量,不同的数值有不同的含义,默认是 -2,当然也可以配置为其他数值,具体数值含义如下:

-1:每个 quicklistNode 节点的 ziplist 所占字节数不能超过 4kb。(建议配置)

-2:每个 quicklistNode 节点的 ziplist 所占字节数不能超过 8kb。(默认配置 & 建议配置)

-3:每个 quicklistNode 节点的 ziplist 所占字节数不能超过 16kb。

-4:每个 quicklistNode 节点的 ziplist 所占字节数不能超过 32kb。

-5:每个 quicklistNode 节点的 ziplist 所占字节数不能超过 64kb。

任意正数:表示 ziplist 结构所最多包含的 entry 个数,最大为215215。

2、quicklistNode 定义

image.png
quicklistNode 字段描述:
prev:指向链表前一个节点的指针

next:指向链表后一个节点的指针

zl:数据指针。如果当前节点的数据没有压缩,那么它指向一个 ziplist 结构;否则,它指向一个 quicklistLZF 结构

sz:表示 zl 指向的 ziplist 的总大小(包括 zlbytes, zltail, zllen, zlend 和各个 entry)。需要注意的是:如果ziplist 被压缩了,那么这个 sz 的值仍然是压缩前的 ziplist 大小

count:表示 ziplist 里面包含的数据项个数。这个字段只有 16bit

encoding:表示 ziplist 是否压缩了(以及用了哪个压缩算法)。目前只有两种取值:2 表示被压缩了(而且用的是 LZF 压缩算法),1 表示没有压缩

container:是一个预留字段。本来设计是用来表明一个 quicklist 节点下面是直接存数据,还是使用 ziplist 存数据,或者用其它的结构来存数据(用作一个数据容器,所以叫 container)。但是,在目前的实现中,这个值是一个固定的值 2,表示使用 ziplist 作为数据容器

recompress:表示当前节点是否是临时使用的解压后的节点,当我们使用类似 lindex 这样的命令查看了某一项本来压缩的数据时,需要把数据暂时解压,这时就设置 recompress=1 做一个标记,等有机会再把数据重新压缩

attempted_compress:这个值只对 Redis 的自动化测试程序有用。我们不用管它

extra:其它扩展字段。目前Redis的实现里也没用上

3、quicklistLZF 定义

image.png
quicklistLZF 字段描述:
sz:压缩后的 ziplist 大小

compressed:柔性数组,存放压缩后的 ziplist 字节数组

LZF 数据压缩的基本思想是:数据与前面重复的,记录重复位置以及重复长度,否则直接记录原始数据内容

三、quicklist 的几个特殊操作简述

1、头插和尾插

头插和尾插会先判断是否能直接在当前头/尾节点插入,如果能就直接插入到对应的 ziplist 里,否则就需要新建一个新节点再操作了。 还记得上文中我们说的 fill 字段吗,其实就是根据 fill 的具体值来判断是否已经超过最大容量。

2、特定位置插入

如果当前被插入节点不满,直接插入。

如果当前被插入节点是满的,要插入的位置是当前节点的尾部,且后一个节点有空间,那就插到后一个节点的头部。

如果当前被插入节点是满的,要插入的位置是当前节点的头部,且前一个节点有空间,那就插到前一个节点的尾部。

如果当前被插入节点是满的,前后节点也都是满的,要插入的位置是当前节点的头部或者尾部,那就创建一个新的节点插进去。

否则,当前节点是满的,且要插入的位置在当前节点的中间位置,我们需要把当前节点分裂成两个新节点,然后再插入。

3、数据删除

删除相对于插入而言简单多了,插入中有节点的分裂,但删除里却没有节点的合并,quicklist 有节点最大容量,但没有最小容量限制,因此直接删除。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值