一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第2天,点击查看活动详情。
概览
Redis 中有序集合 zset 的底层数据结构有 ziplist 和 skiplist 两种实现,当集合内节点相对简单时使用 ziplist 节省空间,当节点数量达到一定规模,或者节点长度超过了设定的阈值,则会转换成 skiplist 跳表。
在上一篇文章中,主要讲解了常规跳表的实现方式,而 Redis 的跳表实现有以下几个特点:
- 上下层级跳跃是通过 level 数组实现,而不是通过 down 指针
- 允许节点的分值相同,分值相同的情况下则通过存储的对象值进行排序
- 引入了 span 跨度的概念,用于计算节点排名
- 支持通过分值,排名的范围查询
- 引入了后退指针,方便对跳表进行倒序遍历
常用命令
假设当前有如下学生的数学成绩需要录入并进行排名:
通过 zadd 命令进行学生成绩录入
通过 zrevrange 命令查询成绩排名前三的学生,并输出对应分数
通过 zrevrangebyscore 命令查询分数在【80,90】区间的学生,并倒序排列
通过 zscore 命令查询某位学生的分数
实现
zset 底层具体的数据结构为 dict + skiplist,dict 主要用来存储成员与分值的映射关系,而 skiplist 则用来有序地存储集合中的成员。
skiplist 的数据结构如下:
- 首,尾节点指针
- 跳表中节点个数
- 当前跳表中节点的最大层数
跳表节点 zskiplistNode 的数据结构如下:
- obj 节点数据,被转成 sds 对象进行存储
- score 节点分值,用于节点间排序,如果两个节点分值一样,则会再根据节点 obj 数据在字典表中的先后位置进行排序
- level[] 存放指向各层链表后一个节点的指针(后向指针),每层对应1个后向指针,用forward字段表示。另外,每个后向指针还对应了一个span值,它表示当前的指针跨越了多少个节点。span用于计算节点排名(rank)
- backward 前向指针,指向前一个节点,用于倒序遍历
整体数据结构如下:
图中红色的箭头也展示了查看分值为89.0 节点的过程,通过将查找路径中跨越的指针 span 值相加即可得出对应的排名 rank = (2 + 2 + 1) - 1 = 4(减1是因为 rank 值从 0 开始)。
如果要得到倒序的排名(从大到小),只需要用skiplist长度减去查找路径上的span累加值,即6-(2+2+1)=1。