Part 1: quicklist 数据结构示意
quicklist 是其实是一种 嵌套 list 类似于C++ 的list 类型的嵌套。std::list<std::list<ele> > list;
quick list 底层 hold 元素的数据类型为 ziplist。所以,简而言之,quicklist 就是节点中 value 类型是ziplist的双链表,不同的是,对外提供的是针对整个quicklist操作ziplist中基础元素的API,这一点与嵌套容器不同,嵌套容器一般都是拿到对应中间层容器,通过操作中间层容器,去操作元素。quicklist的这种方式,将底层隐藏了起来,在有效保持了数据结构平衡的基础上,提供了非常友好的操作接口,方便调用使用这种容器。
另:Redis内部,还提供了一种 LZF 的压缩机制,可以对node中的ziplist整体进行压缩,以节省内存空间。
quick list 示意图
push 操作示意图
quicklist中针对每个ziplist的容量,是有一定的限制,以push head 为例,当该节点中的ziplist“满”的状态下,新元素插入时,将新建节点,然后将新节点插入对应位置,从而避免了整个quicklist的退化(退化成ziplist)。这种内部平衡机制,对用户层是透明的。当然使用quicklist时,应当利用这种机制,有效的发挥quicklist的优势。
quicklistEntry
quicklistEntry:
内部记录一些指针以及状态,遍历quicklist时,记录同步更新记录遍历的准确位置。
另,其中另有三个域,如下:
longval:会直接记录解析的ziplist中存储的整型数。
value 以及 sz 字段: 同时记录ziplist中存储的string类型的首地址以及长度。
quicklistEntry 主要是为了方便 Redis list 数据类型(t_list) 进行遍历,查找索引等操作。
Part 2: insert 操作
1. 当定位到的元素所在的 node(节点) 中的 ziplist 没有达到插入限制条件,insert 实际是 ziplist 的 insert
2. 当定位到的元素所在的 node(节点) 中的 ziplist 已经达到插入限制条件,insert 实际是新建一个新的 node(节点) ,插入到 double list 中。
3. pushHead (lpush) 与 pushTail(rpush) 对 ziplist 插入条件的判断,是类似的。
PS:
注意元素与node(节点)概念的区分。
ziplist 插入限制(quicklist的平衡机制):quicklist fill 字段
每个 quicklist 针对每个 node 设置了一个 16bit 长度的fill 字段,表示每个 node 中的 ziplist 总长度的上限,分为若干级别,默认采用 -2 级别(如上图),也就是 quicklist 单个节点中的 ziplist 总长度,不能超过 8KB。
如果小于 -5, 那么直接取 -5
如果大于 0, 会发生什么呢?
该值 fill 此时会发生语义的改变,成为计数的概念。Redis 内部采取了一个“安全门限”设置,这个值是 8192 (8KB内置常量,无法修改)。即相当于采用了 -2 级别。
并且,在满足安全门限的基础上,会额外增加一个判断,就是 ziplist 中的 entry 数量不得高于这个正数 fill 值。
当然设置正数时不能无限大,因为当配置大于 32768时,直接取 32768(并且此时会失效,安全门限的判断会提前阻断,因为8KB的ziplist根本容不下32768个entry)。
如果为 0,计数条件,无法被满足。此时 ziplist 将退化成仅含一个元素的带header以及tailor的字符串类型,并导致了整个quicklist 退化成为一个 double list。
如果插入限制条件被触发,表示该 node 中无法继续插入元素,那么就会新建 quicklistNode。新 node 将插入进 quicklist 相应位置中。
一旦新建 quicklistNode 节点,就会触发另外一个检查,压缩。
Part 3: 压缩
1. 压缩有额外配置压缩depth检查项,配置如上图,
如果为 0(或者小于0),表示不压缩,默认配置
如果大于零,表示 quicklist 两端分别保留的无需压缩的节点数
默认配置为 0,不压缩,配置为 1,首尾都保持一个节点不压缩
所以,无论如何,head 以及 tail 节点永远都不会被压缩
2. 压缩内容,节点中的整个 ziplist
3. 压缩失败
压缩可能会失败,如下两个 Redis 内置常量值:
#define MIN_COMPRESS_BYTES 48 // ziplist 自身长度低于 48,压缩失败
#define MIN_COMPRESS_IMPROVE 8 // 压缩之后的内存相比原内存,节省空间不足 8 字节,压缩失败
另外,当 quicklist 中的节点数量 低于 压缩depth的 两倍,压缩失败
4. 压缩是不论当前被压缩节点中的 ziplist 是否已经“满”了的
Part 4: 再看插入
LINSERT AFTER/BEFORE list_key pivot_value new_value
LINSERT 命令操作开启了压缩功能的 quicklist。
1. 查找 pivot,就会一边遍历,一般解压,如果当前node中没有,遍历下个节点,当前解压的节点重新压缩。
2. 找到之后,插入限制判断,
2.1 如果当前 ziplist 插入 new_value 不“超限”,则 ziplist insert 该 new_value。
2.2 如果当前 ziplist 插入 new_value 便“超限”,查看插入方向以及 pivot 值索引:
2.2.a. 如果 pivot 处于 ziplist 头部,方向是 before,则试图将 new_value 插入上一个节点中 ziplist 的尾部
2.2.b. 如果 pivot 处于 ziplist 尾部,方向是 after,则试图将 new_value 插入下一个节点中 ziplist 的头部
以上 a b 触发的操作,如果相应的节点中 ziplist 插入 new_value 不“超限”,则对应位置直接插入即可,如果“超限”,则会新建节点并且插入(且触发压缩检查以及相应操作)
2.2.c. pivot 在处于中间位置,怎么办?
pivot 在处于中间位置,怎么办?
1) 当前节点 ziplist 根据 pivot值的索引进行相应的拆分,主要是拆 ziplist.
但是原先 node 与 新建 new_node 的持有部分不同。如上图所示
(注意,这个地方Redis Comment 的描述与代码实现有出入,本文描述是自己看代码得到的结果)
2) 此时查看 LINSERT 命令中是 针对 pivot 的 after 位置还是 before 位置进行操作。
if after: newnode add head
else if before: newnode add tail
newnode to quicklist,注意,也会根据 after 与 before 进行判断,不会破坏原先 元素在quicklist中的顺序。
拆分 node之后,插入 新元素,不会触发 ziplist 插入限制条件检查,会直接插入元素。(此时,整个quicklist的平衡性,其实是遭到一定程度的破坏)
3) nodes try merge
“中心展开”方式进行 nodes 的 merge
merge的判断条件,与节点新增元素逻辑相似。
Part 5: list的使用场景
从前文的结果来看,尽量不要去使用 LINSERT 命令,代价太大。
首先检索,检索的时候,针对存在压缩的情况下,解压缩,遍历查找,后 re-compress,代价极大
其次可能触发节点拆分以及拆分之后的节点合并
另外 list 中数据没有排重,所以 LINSERT 命令未必如我们所愿,能够 insert 到真正想要参考的那个元素的附近
list的优势:
第一:有序,时间顺序,“双端队列”特性
第二:内存利用率高,内部使用 ziplist 存储数据 + compress 机制
第三:跳表形式的结构,对于索引查询,提升了一定的效率
第四:push 以及 pop 操作时间复杂度都是 O(1) 级别
所以使用 list 数据类型,除去 push/pop,其他操作可能都不是很有必要,这样能够发挥 list 数据类型的优势。
从前文来看 插入 操作,内部触发了 quicklist 内部一系列连锁操作,而push 与 pop操作,对于 quicklist 而言代价最小,操作时间稳定:
第一,无需位置索引
第二,仅触发最多一个节点可能的压缩或解压操作
第三,尽可能的保持了quicklist 存储数据的平衡(此时仅头尾节点状态不同,内部节点都保持相对一致),因为,压缩的条件是判断node个数,而非 node 内部 ziplist的状态(长度),由LPUSH 以及 RPUSH 触发的新建节点,以及压缩操作,对 ziplist的容量是有判断的(否则,不会新建节点,便不会触发压缩条件的检查)。
所以,应用层应尽量从两端去操作 list,而避免中间操作。这样的list性能表现更加稳定。
list的不足:
支持的元素类型太简单,只支持string,push/pop对象的功能是没有的,需要自己去逆序列化,或者存入 Redis 中,二次访问去拿hash(此时,其实可以针对 Redis 进行定制,pop出的key值,Redis内部自行检索 hash ,返回给客户端,这样可以节省二次访问带来的延迟以及网络流量的负担)。
其他的一些类似索引修改,索引删除等操作,相比原始的LinkedList有提升,但是与dict相比,还是有差距的。
Redis list的 另一个 advantage:阻塞POP
BLPOP key [key ...] timeout 移除并获取移除元素,或阻塞,直到有一个可用
BRPOP key [key ...] timeout 移除并获取移除元素,或阻塞,直到有一个可用
BRPOPLPUSH source destination timeout 弹出一个列表的值,将它推到另一个列表,并返回它;或阻塞,直到有一个可用
Redis 的作者,Salvatore Sanfilippo,除了喜欢 6379 (这点就跟落魄书生蒲松龄写聊斋一样,YY出一堆美丽的狐狸精,专钟情于穷书生)之外,也非常喜欢 list,给 list 这几个高 B格 的操作。
这几个高逼格的操作,暂且按下,数据结构这里先了解了即可。