本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!
【专栏简介】
随着数据需求的迅猛增长,持久化和数据查询技术的重要性日益凸显。关系型数据库已不再是唯一选择,数据的处理方式正变得日益多样化。在众多新兴的解决方案与工具中,Redis凭借其独特的优势脱颖而出。
【技术大纲】
为何Redis备受瞩目?原因在于其学习曲线平缓,短时间内便能对Redis有初步了解。同时,Redis在处理特定问题时展现出卓越的通用性,专注于其擅长的领域。深入了解Redis后,您将能够明确哪些任务适合由Redis承担,哪些则不适宜。这一经验对开发人员来说是一笔宝贵的财富。
在这个专栏中,我们将专注于Redis的6.2版本进行深入分析和介绍。Redis 6.2不仅是我个人特别偏爱的一个版本,而且在实际应用中也被广泛认为是稳定性和性能表现都相当出色的版本。
【专栏目标】
本专栏深入浅出地传授Redis的基础知识,旨在助力读者掌握其核心概念与技能。深入剖析了Redis的大多数功能以及全部多机功能的实现原理,详细展示了这些功能的核心数据结构和关键算法思想。读者将能够快速且有效地理解Redis的内部构造和运作机制,这些知识将助力读者更好地运用Redis,提升其使用效率。
将聚焦于Redis的五大数据结构,深入剖析各种数据建模方法,并分享关键的管理细节与调试技巧。
【目标人群】
Redis技术进阶之路专栏:目标人群与受众对象,对于希望深入了解Redis实现原理底层细节的人群。
1. Redis爱好者与社区成员
Redis技术有浓厚兴趣,经常参与社区讨论,希望深入研究Redis内部机制、性能优化和扩展性的读者。
2. 后端开发和系统架构师
在日常工作中经常使用Redis作为数据存储和缓存工具,他们在项目中需要利用Redis进行数据存储、缓存、消息队列等操作时,此专栏将为他们提供有力的技术支撑。
3. 计算机专业的本科生及研究生
对于学习计算机科学、软件工程、数据分析等相关专业的在校学生,以及对Redis技术感兴趣的教育工作者,此专栏可以作为他们的学习资料和教学参考。
无论是初学者还是资深专家,无论是从业者还是学生,只要对Redis技术感兴趣并希望深入了解其原理和实践,都是此专栏的目标人群和受众对象。
让我们携手踏上学习Redis的旅程,探索其无尽的可能性!
跳跃表
跳跃表(Skiplist)是一种别具一格的有序数据结构,其核心魅力在于节点内部巧妙地存储了多个指向其他节点的指针。这一设计使得节点访问机制得以高效实现,极大地提升了数据检索与操作的性能。其逻辑模型如以下图示所示,直观展现了其结构的层次性与灵活性。
刚才,我为大家展示了通过(1->2->3->4)的步骤,详细地勾勒出寻找特定元素的整个流程。此外,这种数据结构还具备强大的数据处理能力,可以轻易地返回特定范围内的数据信息。它的精妙之处在于巧妙地平衡了查询速度与内存开销之间的关系,从而在实际应用中展现出了出色的性能。
跳跃表的性能
在平均情况下,该数据结构能以O(log W)的复杂度迅速找到目标节点,即便在最不利的情况下,其查找复杂度也仅为O(W),这足以证明其高效性与稳定性。
在多数场景中,跳跃表的表现足以与平衡树相媲美,甚至在效率上不相上下。更值得一提的是,相较于平衡树的复杂实现,跳跃表的实现更为简洁明了,这使得许多程序选择采用跳跃表作为平衡树的替代方案。
Redis的有序集合
Redis选用了跳跃表作为其有序集合(Sorted Set / ZSet)的底层实现方式之一,这一选择凸显了跳跃表在数据处理方面的独特优势。有序集合不仅继承了集合的基本特性,如元素的唯一性和不重复性,还进一步扩展了对元素权重的支持。这使得有序集合能够根据元素的权重进行排序,从而实现了更为灵活和高效的数据管理方式。
权重和范围数据检索
- 【范围检索】:
ZRANGEBYSCORE
命令凭借其根据元素权重检索指定范围内元素的能力,为我们提供了一种高效且灵活的数据检索方式。这一特性使得在处理大量数据时,我们能够快速定位并获取所需范围内的元素,从而极大地提升了数据处理效率和准确性。
shell
// 获取5-15范围内的数据集合
ZRANGE dataSet 5 15 WITHSCORES
- 【排序权重】:
ZSCORE
命令为我们提供了一个便捷的途径,使我们能够迅速检索并返回特定元素的权重值,从而轻松获取关于元素权重的详细信息。这一功能在数据管理和操作中显得尤为实用,有助于我们更全面地了解和使用数据。
shell
// 查询dataSet中的element数据的权重值
ZSCORE dataSet element
这些特性使得有序集合在实际应用中具有广泛的用途,比如实现排行榜、成绩单等功能。
具体来说,当有序集合中包含的元素数量较多,或者有序集合中的元素成员为较长的字符串时,Redis会倾向于采用跳跃表作为底层数据结构,这是因为跳跃表在处理大量数据或复杂数据时,能够展现出优秀的查询和访问性能,从而确保Redis在这些场景下依然能够保持高效和稳定的工作状态。
Redis有序集合 --- 跳跃表的实现
Redis的跳跃表在源码中:server.h/zskiplistNode
和server.h/zskiplist
两个结构进行定义,其中zskiplistNode
结构专门用来表征跳跃表中的节点,而zskiplist
结构则用于存储关于跳跃表节点的相关元数据,如节点的总数、指向表头节点和表尾节点的指针等。
在server.h文件中基本定义的源码如下:
c
/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
sds ele;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned long span;
} level[];
} zskiplistNode;
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
- 对应的源码地址:github.com/redis/redis...
这种设计使得跳跃表在Redis内部得以高效且有序地管理数据,为有序集合和集群节点等功能的实现提供了坚实的基础。
zskiplistNode
在上面的zskiplist结构的展示了6个zskiplistNode结构的实例,下面便是对应的源码定义:
c
/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
// sorted set 中的元素
sds ele;
// 元素权重
double score;
// 后向指针(为了便于从跳表的尾节点倒序查找)
struct zskiplistNode *backward;
// 节点的 level 数组
struct zskiplistLevel {
// 每层上的前向指针
struct zskiplistNode *forward;
// 跨度,记录节点在某一层 *forward 指针和该节点,跨越了 level0 上的几个节点
unsigned long span;
} level[];
} zskiplistNode;
每个节点都具备以下关键属性:
- 层级(Level):节点中的L1、L2、L3等标记代表了节点的不同层级,其中L1代表第一层,L2代表第二层,依此类推。
每一层都包含两个核心属性:前进指针和跨度(span)。 在图中,前进指针用于指向表尾方向的下一个节点,而跨度则记录了当前节点与前进指针所指节点之间的元素数量。
c
typedef struct zskiplist {
// 头节点和尾节点
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
当程序需要从表头遍历至表尾时,会沿着各层的前进指针进行。
-
后退(Backward)指针 :在节点中,BW标记即为后退指针,它指向当前节点的前一个节点。这一指针在程序需要从表尾遍历至表头时发挥着关键作用。 这样的设计使得跳跃表能够在不同的遍历方向上保持高效,无论是从表头到表尾,还是从表尾到表头,都能快速定位并访问相应的节点。
-
分值(score):节点的分值(score属性)是double类型的浮点数,用于标识跳跃表中各节点所保存的值。节点依据分值大小进行排序,确保整个跳跃表按照分值从小到大的顺序排列,从而维护了有序性。
-
成员对象(obj) :节点的成员对象(
obj
属性)是一个精心设计的指针,它精准地指向一个字符串对象。这个字符串对象,则是SDS(简单动态字符串)值的温馨居所,负责安全、高效地存储和管理这些重要的数据。
层级(Level)
跳跃表节点的Level数组可以包含多个元素,每个元素均指向其他节点,程序正是通过这些层来加速对其他节点的访问。一般而言,层的数量越多,访问其他节点的速度便越快。
下图展示了4个不同高度的节点,它们的层数分别为2层、1层和2层、1层。 在创建新的跳跃表节点时,程序会根据幂次定律(即数值越大,其出现的概率越小)随机生成一个介于1和32之间的数值,作为level数组的大小,这个大小即为层的"高度"。
遍历搜索处理
1)首先访问跳跃表的起始点------表头节点,随后通过第1层的指针迅速跃迁到第2个节点。
2)到第2个节点后,程序并未继续向上层跃迁,而是选择沿着第2层的指针稳步前进,顺利到达第4个节点。
3)到第4个节点处,程序继续沿用相同的策略,对比之后,退回到即第2个节点沿着第5层的前进指针,平稳地转移至第4个节点。
4)当程序尝试从第4个节点继续沿着前进指针移动时,到了第5个节点。
注意,由于C语言的数组索引是从0开始的,因此节点的第一层对应的是level[0],第二层对应的是level[1],以此类推。这样的设计使得跳跃表在保持数据有序性的同时,能够高效地执行插入、删除和查找等操作。
跨度(Span)
层的跨度是用来记录相邻节点间的距离度量:当两个节点之间的跨度值越大,表明它们相隔的物理位置越远。跨度的主要作用在于计算节点的排位(rank
)。在查找特定节点的过程中,程序会累加所经过各层的跨度值,最终得到的结果便是目标节点在跳跃表中的确切排位。
可以看到上面的图,可以得出第一个节点的Span=0,第二个span=(1),第三个节点的span=(1+1),第四个节点的span=(1+2),以此类推即可。
注意,所有指向
NULL
的前进指针的跨度均被设定为0,因为它们并未指向任何有效的节点。
后退指针(backward)
节点的后退指针(
backward
属性)在从跳跃表尾部向头部方向进行节点访问时发挥着关键作用。
每个节点仅配有一个后退指针,因此每次只能逐步后退至紧邻的前一个节点,从跳跃表尾部逆向遍历至头部的节点路径。
借助跳跃表的tail
指针快速定位到表尾节点,随后利用该节点的后退指针访问其前一个节点,即倒数第二个节点。接着,继续沿着后退指针逐步访问。
zskiplist
多个跳跃表节点就可以组成一个跳跃表,通过使用zskiplist结构来管理这些节点,程序能够更高效地处理整个跳跃表。这一结构不仅使得程序能够迅速访问跳跃表的表头节点和表尾节点,还能迅速获取跳跃表中节点的数量(即跳跃表的长度)等关键信息。
下图为zskiplist结构。 该结构包含以下核心属性:
c
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
- header:一个指向跳跃表表头节点的指针,用于快速定位跳跃表的起始位置。
- tail:一个指向跳跃表表尾节点的指针,便于从尾部对跳跃表进行操作或遍历。
- level:记录当前跳跃表中层数最多的节点的层数。
- length:表示跳跃表的长度,即跳跃表当前包含的节点数量。
最终总结
跳跃表是有序集合的底层实现机制之一,Redis的跳跃表实现精巧地由zskiplist和zskiplistNode两个结构共同构成。其中,zskiplist结构负责保存整个跳跃表的关键信息,如表头节点、表尾节点以及长度等,而zskiplistNode结构则用于具体表示跳跃表中的每一个节点。
跳跃表节点
每个跳跃表节点的层高并非固定,而是根据幂次定律随机生成的介于1至32之间的数值。这种设计既保证了跳跃表的灵活性,又能在一定程度上优化查找效率。在同一个跳跃表中,虽然多个节点可能包含相同的分值,但每个节点的成员对象必须保持唯一性,这确保了数据的准确性和一致性。
跳跃表链表
跳跃表中的节点严格按照分值大小进行排序。当分值相同时,节点则会根据成员对象的大小进一步排序,这种精细化的排序机制确保了跳跃表在处理复杂数据时依然能够保持高效和有序。
header和tail指针
- header和tail指针分别精准地指向跳跃表的起始和结束节点,这一设计确保了程序能够在O(1)复杂度内迅速定位到表头节点和表尾节点,极大提高了处理效率。
- length属性精确记录跳跃表中节点的数量,使得程序在O(1)复杂度内就能轻松获取跳跃表的长度。
- level属性则负责在O(1)复杂度内迅速提供跳跃表中层高最大节点的层数信息,