携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第23天,点击查看活动详情
跳表
只有zset对象的底层实现用到了跳表,跳表支持O(logn)复杂度的节点查找。
zset底层有跳表和哈希表。跳表拿来范围查询,哈希表用来常数复杂度获取权重。
跳表结构设计
多层的有序链表
跳表结构体
zskiplist
- head
- tail
- length 节点数量
- level 跳表的最大层数,层高最大的节点的层数量
跳表节点数据结构
zskiplistNode:
- sds ele 对象元素值
- score 权重值
- zskiplistNode *backward; 后向指针 方便倒序查找
- level数组 每层的前向指针和跨度 zskiplistNode *forward 和span,跨度实际上是为了计算节点在跳表中的排位
跳表节点的查询过程
跳表会从头节点的最高层开始,遍历每一层,根据SDS元素和权重判断:
- 当前节点权重小于要查找的权重,访问该层的下一个节点
- 当前节点权重等于要查找的权重,SDS类型数据小于要查找的数据,访问该层的下一个节点
都不满足或者下一个节点为空,遍历当前节点level数组的下一层。
\
跳表节点层数设置
跳表的相邻两层的节点数量最理想的比例是 2:1,查找复杂度可以降低到 O(logN)。
如何维持?
跳表在创建节点的时候,随机生成每个节点的层数,
具体的做法是,跳表在创建节点时候,会生成范围为[0-1]的一个随机数,如果这个随机数小于 0.25(相当于概率 25%),那么层数就增加 1 层,然后继续生成下一个随机数,直到随机数的结果大于 0.25 结束,最终确定该节点的层数。
这样的做法,相当于每增加一层的概率不超过 25%,层数越高,概率越低,层高最大限制是 64
为什么用跳表而不用平衡树(如 AVL树、红黑树等)?
- 它们不是非常内存密集型,跳表占用更少内存
- 范围查找,链表遍历,跳表的缓存局部性比其他类型的树好
- 易于调试,结构简单。
quicklist
3.2list底层由quicklist实现
双向链表+压缩列表。quicklist实际是一个链表,每个元素是一个压缩列表
quicklist
- head
- tail
- count 总元素个数
- len quciklistnode 的个数
quicklist节点结构
quicklistnode:
- prev 前一个节点
- next 下一个节点
- zl 指向的压缩列表
- sz 压缩列表字节大小
- count 压缩列表元素个数
\
插入元素时,先检查插入位置的压缩列表是否能够容纳,不能就新建链表节点。
quicklist会控制压缩列表的大小和元素个数,规避连锁更新,并没有完全解决连锁更新
listpack
要想彻底解决连锁更新,需要一个新的数据结构
listpack结构设计
采用了压缩列表优秀的设计,比如连续的内存空间来保存数据,采用不同编码方式保存不同大小的数据。
- encoding,定义该元素的编码类型,会对不同长度的整数和字符串进行编码;
- data,实际存放的数据;
- len,encoding+data的总长度;
可以看到,listpack 没有压缩列表中记录前一个节点长度的字段了,listpack 只记录当前节点的长度,当我们向 listpack 加入一个新元素的时候,不会影响其他节点的长度字段的变化,从而避免了压缩列表的连锁更新问题。