Redis Source Code Read Log( 10 data type: list 之 quicklist )

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(并且此时会失效,安全门限的判断会提前阻断,因为8KBziplist根本容不下32768entry)

如果为 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 insertnew_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的不足:

支持的元素类型太简单,只支持stringpush/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的操作。

这几个高逼格的操作,暂且按下,数据结构这里先了解了即可。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值