深入理解 Redis跳跃表 Skip List 原理|图解查询、插入

1. 简介

跳跃表 ( skip list ) 是一种有序数据结构,通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。

在 Redis 中,跳跃表是有序集合键的底层实现之一,那么这篇文章我们就来讲讲跳跃表的实现原理。

2. 跳跃表的实现

Redis 的跳跃表由 redis.h/zskiplistNoderedis.h/zskiplist 两个结构定义。

  • zskiplistNode 结构用于表示跳跃表节点。
  • zskiplist 结构则用于保存跳跃表节点的信息,比如节点数量指向表头节点和表尾节点的指针等等。

上图中展示了一个跳跃表示例,最左边的就是 zskiplist 结构,各个字段含义如下:

  • Header:指向跳跃表的表头节点
  • Tail:指向跳跃表的表尾节点
  • Level:记录目前跳跃表内,层数最大的那个节点的层数(除了头节点)
  • Length:记录跳跃表的长度,也就是跳跃表目前包含节点的数量(除了头节点)
  • 层(level):节点中用L1、L2、L3等字样标记节点的各个层,L1表示第一层,L2代表第二层,以此类推。每层都带有两个属性:前进指针和跨度前进指针用于方位位于表尾方向的其他节点。而跨度则记录了前进指针所指向节点和当前节点的距离。
  • backward:后退指针, 节点中用BW字样标记的后退指针 ,他指向当前节点的前一个节点。后退指针在程序从表尾向表头遍历时使用。
  • score:分值,各个节点中的 1.0、2.0、3.0 是节点所保存的分值。节点会按各个所保存的分值从小到大排序。
  • obj:成员对象,各个节点中的o1,o2 和 o3 是节点所保存的成员对象。

3. 跳跃表节点

跳跃表节点的实现由 redis.h/zskiplistNode 结构定义,数据结构如下:

c 复制代码
typedef struct zskiplistNode{
	struct zskiplistNode *backward;	// 后退指针
	double score; 	// 分支
	robj *obj;		// 成员对象
	struct zskiplistLevel { // 层
		struct zskiplistNode *forward; // 前进指针
		unsigned int span;	// 跨度
	} level[];
}zskiplistNode;

3.1 层

跳跃表节点的 level 数组可以包含多个元素,每个元素都包含一个指向其他节点的指针,程序可以通过这些层来加快访问其他节点的速度,一般来说,层的数量越多,访问其他节点的速度就越快。

3.2 前进指针

每个层都有一个指向表尾方向的前进指针,用于从表头向表尾方向访问节点。那么如何查到对应的某个值呢?

比如有以下这个跳跃表结构:我们需要查到到7这个元素

  1. 会从最高层,也就是L5开始跳,绿色的箭头跳到了下一层的L5,发现这个值是16,16>7,所以不再继续在这一层跳,走下一层。
  2. L5跳到L4
  3. L4第一跳发现是2,比7大
  4. 于是接着L4的第二条,发现是16,比7小,所以不再继续在L4跳,走下一层
  5. L4跳L3,注意这里L4是在元素2的节点跳的L3
  6. L3接着一跳发现是16,比7大,所以不再继续L3层跳,走下一层
  7. L3跳L2
  8. L2 一跳找到了7,于是就返回了。

3.3 插入元素

我们知道了如何将查到到这个元素,之后,那如何插入呢?比如在上一个查询例子上插入元素10,那么我们可以先按照上面的方法找到10。

我们按照上面的方式找到L1之后还是没有找到10,于是就可以在最低层的7~16之间做插入。每次创建一个新跳跃表节点的时候,程序都根据幂次定律,随机生成一个介于1和32之间的值作为level数组的大小,这个大小就是层的高度。

假设这一次生成的层高是2,那么最高层就是L2。

此时跳表的结构就要发现变化,7的L2的下一跳变成了10的L2,10的L2的下一跳是16的L2,以此类推

这就插入成功了。

3.4 跨度

层的跨度用于记录两个节点之间的距离

  • 两个节点之间的跨度越大,它们相距就得越远。
  • 指向NULL的所有前进指针的跨度都为0,因为他们没有连向任何节点。

下图中的黑色字体就是跨度

这里注意一点:遍历操作只使用前进指针就可以完成了,跨度实际上是用来计算排位的,在查找某个节点的过程中,讲沿途访问过的所有层的跨度累计起来,得到的结果就是目标节点在跳跃表中的排位。

3.5 后退指针

节点的后退指针用于从表尾向表头方向访问节点:和可以一次跳过多个节点的前进指针不同,因为每个节点只有一个后退指针,所以每次都只能后退至前一个节点。

用红色虚线展示了如果从表尾向表头遍历跳跃表中的所有节点:程序首先通过跳跃表的tail指针 访问表尾节点,然后通过后退指针访问倒数第二个节点,之后再沿着后退指针访问倒数第三个节点,在之后遇到指向NULL的后退指针,于是访问结束。

3.6 分值和对象

  • score:分值是一个double类型的浮点数,跳跃表中的所有节点都按分值从小到大来排序。
  • obj对象:节点的成员对象是一个指针,它指向一个字符串对象,而字符串对象则保存着一个SDS值。

在同一个跳跃表中,各个节点保存的成员对象必须是唯一的,但是多个节点保存的分值却可以是相同的:分值相同的节点将按照成员对象在字典序中的大小来进行排序成员对象较小的节点会排在前面(靠近表头的方向),而成员对象较大的节点则会排到后面(靠近表尾的方向)

4. 跳跃表

仅靠多个跳跃表节点就可以组成一个跳跃表,但通过使用一个 zskiplist 结构来持有这些节点,程序可以更方便对整个跳跃表进行处理,比如快速访问跳跃表的表头节点和表尾节点,或者快速地获取跳跃表节点地数量(也即是跳跃表的长度)等信息。

c 复制代码
typedef struct zskiplist{
	structz skiplistNode *header,*tail; 	// 表头节点和表尾节点
	unsigned long length; // 表中节点的数量
	int level; // 表中层数最大的节点的层数
}zskiplist;
  1. header 和 tail 指针分别指向跳跃表的表头和表尾节点,通过这两个指针,程序定位表头节点和表尾节点的复杂度未O(1)。
  2. 通过 length 属性来记录节点的数量,程序可以在O(1)复杂度内返回跳跃表的长度。
  3. level 属性则用于在 O(1)复杂度内获取跳跃表中层高最大的那个节点的层数量,注意表头节点的层高并不计算在内。

本来想讲讲为什么mysql用B+树不是跳表、而redis用跳表不用B+树。 但是篇幅有限,我们留到下一篇文章再讲了。

相关推荐
岁岁种桃花儿35 分钟前
MySQL从入门到精通系列:InnoDB记录存储结构
数据库·mysql
jiunian_cn2 小时前
【Redis】hash数据类型相关指令
数据库·redis·哈希算法
冉冰学姐2 小时前
SSM在线影评网站平台82ap4(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面
数据库·ssm框架·在线影评平台·影片分类
知识分享小能手3 小时前
SQL Server 2019入门学习教程,从入门到精通,SQL Server 2019数据库的操作(2)
数据库·学习·sqlserver
踩坑小念4 小时前
秒杀场景下如何处理redis扣除状态不一致问题
数据库·redis·分布式·缓存·秒杀
萧曵 丶5 小时前
MySQL 语句书写顺序与执行顺序对比速记表
数据库·mysql
Wiktok5 小时前
MySQL的常用数据类型
数据库·mysql
曹牧6 小时前
Oracle 表闪回(Flashback Table)
数据库·oracle
浪客灿心6 小时前
list_stack_queue
数据结构·list
J_liaty6 小时前
Redis 超详细入门教程:从零基础到实战精通
数据库·redis·缓存