Redis-有序集合之跳表实现

508 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 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。

推荐阅读