redis之数据结构与对象篇(三)

欢迎阅读大魔王的睡前私语系列,这是Redis第三篇文章

跳跃表

跳跃表是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。跳跃表支持平均O(logN),最坏O(N)复杂度的节点查找,还可以通过顺序性操作批量处理节点。

Redis使用跳跃表来作为有序集合键的底层实现之一,如果一个有序集合博阿寒的元素数量比较多,或者有序集合中元素的成员是比较长的字符串,Redis就会使用跳跃表来作为有序集合键的底层实现。

和链表,字典等数据结构不一样,Redis只在两个地方用到了跳跃表,一个是实现有序集合键,另外一个是集群节点中用于内部数据结构,除此之外,跳跃表在Redis里面不会有其他用途。

跳跃表实现

Redis的跳跃表由redis.h/zskiplisNode和redis.h/zskiplist两个结构定义,其中zskiplistNode结构用于表示跳跃表节点,而zskiplist结构则用于保存跳跃表节点信息,比如节点的数量,以及指向表头节点和表尾节点的指针。

下图是一个跳跃表实例:
在这里插入图片描述
位于图片最左边的是zskiplist结构,该结构包含以下属性:

  • header:指向跳跃表的表头节点
  • tail:指向跳跃表的表尾节点
  • level:记录目前跳跃表内,层数最大的哪个节点的层数(表头节点的层数不计算在内)
  • length:记录跳跃表的长度,即跳跃表目前包含节点的数量

其他的代表zskipistNode结构,该结构包含以下属性:

  • 层:节点中用L1,L2,L3等字样标记节点的各个层,L1代表第一层,L2代表第二层等等。每个层都带有两个属性:前进指针和跨度。前进指针用于访问位于表尾方向的其他节点,而跨度则记录了前进指针所指向节点和当前节点的距离,上图中,连线上带有数字的箭头代表前进指针,而那个数字代表的是跨度。当程序从表头从表尾进行遍历时,访问会沿着层的前进指针进行。
  • 后退指针:节点中用BW字样标记节点的后退指针,它指向位于当前节点的前一个节点。后退指针在程序从表尾向表头遍历时使用
  • 分值(score):各个节点中的1.0,2.0,3.0是各个节点保存的分值,在跳跃表中,节点按各自所保存的分值从小到大排序。
  • 成员对象:各个节点中的o1,o2,o3是节点保存的成员对象。

跳跃表节点

跳跃表节点的实现由redis.h/zskiplistNode结构定义:

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

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

每次创建一个新跳跃表节点的时候,程序根据幂次定律(power law,越大的数出现的概率越小)随机生成一个介于1和32的值作为level数组的大小,这个大小就是层的高度。

前进指针

每个曾都有一个指向表尾方向的前进指针(level[i].forward属性),用于从表头向表尾方向访问节点。

在这里插入图片描述

  • 迭代程序首先访问跳跃表的第一个节点(表头),然后从第三层的前进指针移动到表中的第二个节点
  • 在第二个节点,程序沿着第二层的前进指针移动到表中的第三个节点
  • 在第三个节点时,程序同样沿着第二层的前进指针移动到表的第四个节点
  • 当程序再沿着第四个节点的前进指针移动时,它碰到一个NULL,程序直到这时已经到达了跳跃表的表尾,于是结束这次遍历

跨度

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

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

跨度实际是用来计算排位的:在查找某个节点的过程中,沿途将访问过的所有层的跨度累计在一起,得到的结果就是目标节点在跳跃表的排位

后退指针

节点的后退指针(backward属性)用于从表尾向表头方向访问节点:跟可以一次跳过多个节点的前进指针不同,每个节点只有一个后退指针,所以每次只能后退至前一个节点

分值和成员

节点的分值(score)是一个double类型的浮点数,跳跃表中的所有节点都按分值从小到大排序

节点的成员对象是一个指针,它指向一个字符串对象,而字符串对象则存在一个SDS值

在同一个跳跃表中,各个节点保存的成员对象必须是唯一的但是多个节点保存的分值却可以是相同得:分值相同得节点将按照成员对象在字典序中的大小进行排序,成员对象较小的节点会排在前面,成员对象较大的节点排在后面

跳跃表

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

zskiplist结构定义如下:

typedef struct zskiplist{
	structz skiplistode *header,*tail
	//表姐点的数量
	unsigned long length;
	//表中层数最大的节点的层数
	int level;
}zskiplist;

header它tail指针分别指向跳跃表的表头和表尾节点,通过这两个指针,程序定位表头节点和表尾节点的复杂度为O(1)
通过使用length属性记录节点的数量,程序在O(1)复杂度内返回跳跃表的长度
level书香则用于在O(1)复杂度内获取跳跃表中层最高的那个节点得到层数量,注意头节点的层高不再计算范围内。

整数集合

整数集合是集合键的底层实现之一 ,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现。

整数集合的实现

整数集合(intset)用于保存整数值的集合抽象数据结构它可以保存整数值,并且保证集合中不会出现重复元素。

typedef struct intset{
	//编码方式
	uint32_t encoding;
	//集合包含的元素数量
	uint32_t length;
	//保存元素的数组
	int8_t contents[];
}

length属性记录整数集合包含得元素数量,即contents数组的长度

contents数组是整数集合的底层实现:整数集合的每一个元素都是contents数组的一个数组项,各个项在数组中按值得大小从小到大排序,并且数组不包含任何重复项

虽然inset结构将contents属性声明为int8_t类型数组,但contents数组正真的类型取决于enconding属性的值。

升级

当我们将一个新的元素添加到整数集合中,并且新的元素的类型比整数集合现在的类型要长时,整数集合需要先进行升级,然后将新元素类型添加到整数集合上。

升级分为三步:

  1. 根据新的元素类型,扩展整数集合底层的空间大小,为新的元素分配空间
  2. 将底层数组现有的元素转换成新元素相同的类型,并且将类型转换后的元素放到正确的位置上,在放置元素过程中,保持有序性
  3. 将新元素放到底层数组中

因为每次向整数集合添加新元素都可能会引起升级,而每次升级都将对底层数组中已有的所有元素·进行类型转换,所以向整数集合添加新元素的时间复杂度为O(N)

升级后新元素的位置

因为引发升级的新元素的场地总是比整数集合现有的所有元素的长度都大,所以这个新元素要么大于所有现有元素,要么大于所有现有元素。

升级的好处

整数集合的升级策略有两个好处,一个时提升整数集合的灵活性,另一个尽可能节省内存。

提升灵活性

因为C语言是静态类型的语言,为了避免类型错误,通常不会将两种不同类型的值存放在同一数据结构中。但是整数集合可以通过自动升级来适应新元素,所以我们可以随意地将不同类型地整数都添加到几何中,而不必担心类型错误。

节省内存

一个数组同时保存int6,int32,int64三种类型地值,最简单地做法是直接使用int64类型地数组直接作为数组集合的底层实现,但这样会造成空间的浪费,而整数集合现在的做法可以让集合能同时保存三种不同类型的值,又可以确保升级操作只在需要的时候进行,这样可以节省内存。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值