Redis5设计与源码分析 (第3章 跳跃表)
Posted 将军上座
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis5设计与源码分析 (第3章 跳跃表)相关的知识,希望对你有一定的参考价值。
3.1 简介
跳跃表有如下性质。
1)跳跃表由很多层构成。
2)跳跃表有一个头(header)节点,头节点中有一个64层的结构,每层的结构包含指向本层的下个节点的指针,指向本层下个节点中间所跨越的节点个数为本层的跨度(span)。
3)除头节点外,层数最多的节点的层高为跳跃表的高度(level),图3-3中跳跃表的高度为3。
4)每层都是一个有序链表,数据递增。
5)除header节点外,一个元素在上层有序链表中出现,则它一定会在下层有序链表中出现。
6)跳跃表每层最后一个节点指向NULL,表示本层有序链表的结束。
7)跳跃表拥有一个tail指针,指向跳跃表最后一个节点。
8)最底层的有序链表包含所有节点,最底层的节点个数为跳跃表的长度(length)(不包括头节点),图3-3中跳跃表的长度为7。
9)每个节点包含一个后退指针,头节点和第一个节点指向NULL;其他节点指向最底层的前一个节点。
跳跃表每个节点维护了多个指向其他节点的指针,所以在跳跃表进行查找、插入、删除操作时可以跳过一些节点,快速找到操作需要的节点。归根结底,跳跃表是以牺牲空间的形式来达到快速查找的目的。跳跃表与平衡树相比,实现方式更简单,只要熟悉有序链表,就可以轻松地掌握跳跃表。
3.2 跳跃表节点与结构
3.2.1 跳跃表节点
跳跃表节点的zskiplistNode结构体。参考3-3图进行分析;
typedef struct zskiplistNode {
sds ele;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward; //指向本层下一个节点
unsigned long span; // 跳过的节点个数
} level[];
} zskiplistNode;
该结构体包含如下属性。
1)ele:用于存储字符串类型的数据。
2)score:用于存储排序的分值。
3)backward:后退指针,只能指向当前节点最底层的前一个节点,头节点和第一个节点——backward指向NULL,从后向前遍历跳跃表时使用。
4)level:为柔性数组。每个节点的数组长度不一样,在生成跳跃表节点时,随机生成一个1~64的值,值越大出现的概率越低。level数组的每项包含以下两个元素。
·forward:指向本层下一个节点,尾节点的forward指向NULL。
·span:forward指向的节点与本节点之间的元素个数。span值越大,跳过的节点个数越多。
跳跃表是Redis有序集合的底层实现方式之一,所以每个节点的ele存储有序集合的成员member值,score存储成员score值。所有节点的分值是按从小到大的方式排序的,当有序集合的成员分值相同时,节点会按member的字典序进行排序。
3.2.2 跳跃表结构
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
1)header:指向跳跃表头节点。头节点是跳跃表的一个特殊节点,它的level数组元素个数为64。头节点在有序集合中不存储任何member和score值,ele值为NULL,score值为0;也不计入跳跃表的总长度。头节点在初始化时,64个元素的forward都指向NULL,span值都为0。
2)tail:指向跳跃表尾节点。
3)length:跳跃表长度,表示除头节点之外的节点总数。
4)level:跳跃表的高度。
通过跳跃表结构体的属性我们可以看到,程序可以在O(1)的时间复杂度下,快速获取到跳跃表的头节点、尾节点、长度和高度。
3.3 基本操作
3.3.1 创建跳跃表
1.节点层高
节点层高的最小值为1,最大值是ZSKIPLIST_MAXLEVEL,
Redis5中节点层高的值为64。Redis6中为32。
#define ZSKIPLIST_MAXLEVEL 64
Redis通过zslRandomLevel函数随机生成一个1~64的值,作为新建节点的高度,值越大出现的概率越低。节点层高确定之后便不会再修改。生成随机层高的代码如下。
#define ZSKIPLIST_P 0.25 /* Skiplist P = 1/4 */
int zslRandomLevel(void) {
int level = 1;
while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
level += 1;
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
level的初始值为1,通过while循环,每次生成一个随机值,取这个值的低16位作为x,当x小于0.25倍的0xFFFF时,level的值加1;否则退出while循环。最终返回level和ZSKIPLIST_MAXLEVEL两者中的最小值。
下面计算节点的期望层高。假设p=ZSKIPLIST_P:
1)节点层高为1的概率为(1-p)。
2)节点层高为2的概率为p(1-p)。
3)节点层高为3的概率为p^ 2 (1-p)。
4)……
5)节点层高为n的概率为p ^n-1 (1-p)。
所以节点的期望层高为
当p=0.25时,跳跃表节点的期望层高为1/(1-0.25)≈1.33。
2.创建跳跃表节点
跳跃表的每个节点都是有序集合的一个元素,在创建跳跃表节点时,待创建节点的层高、分值、member等都已确定。对于跳跃表的每个节点,我们需要申请内存来存储,代码如下。
zskiplistNode *zslCreateNode(int level, double score, sds ele) {
zskiplistNode *zn =
zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel));
// 分配好空间之后,进行节点变量初始化。
zn->score = score;
zn->ele = ele;
return zn;
}
zskiplistNode结构体的最后一个元素为柔性数组,申请内存时需要指定柔性数组的大小,一个节点占用的内存大小为zskiplistNode的内存大小与level个zskiplistLevel的内存大小之和。
3.头节点
不存储有序集合的member信息。头节点是跳跃表中第一个插入的节点,其level数组的每项forward都为NULL,span值都为0。
for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) {
zsl->header->level[j].forward = NULL;
zsl->header->level[j].span = 0;
}
4.创建跳跃表的步骤
创建完头节点后,就可以创建跳跃表。创建跳跃表的步骤如下。
1)创建跳跃表结构体对象zsl。
2)将zsl的头节点指针指向新创建的头节点。
3)跳跃表层高初始化为1,长度初始化为0,尾节点指向NULL。
zskiplist *zslCreate(void) {
int j;
zskiplist *zsl;
zsl = zmalloc(sizeof(*zsl));
zsl->level = 1;
zsl->length = 0;
zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL);
for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) {
zsl->header->level[j].forward = NULL;
zsl->header->level[j].span = 0;
}
zsl->header->backward = NULL;
zsl->tail = NULL;
return zsl;
}
3.3.2 插入节点
插入节点的步骤:
①查找要插入的位置;②调整跳跃表高度;③插入节点;④调整backward。
查找插入位置代码:
// 两个长度为64的数组来辅助操作
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
unsigned int rank[ZSKIPLIST_MAXLEVEL];
int i, level;
serverAssert(!isnan(score));
x = zsl->header;
for (i = zsl->level-1; i >= 0; i--) {
/* store rank that is crossed to reach the insert position */
rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
while (x->level[i].forward &&
(x->level[i].forward->score < score ||
(x->level[i].forward->score == score &&
sdscmp(x->level[i].forward->ele,ele) < 0)))
{
rank[i] += x->level[i].span;
x = x->level[i].forward;
}
update[i] = x;
}
update[]:插入节点时,需要更新被插入节点每层的前一个节点。由于每层更新的节点不一样,所以将每层需要更新的节点记录在update[i]中。
rank[]:记录当前层从header节点到update[i]节点所经历的步长,在更新update[i]的span和设置新插入节点的span时用到。
for循环递减层数,while 循环找同层下一个合适的节点位置 (下一个节点score<新的score,或者下一个节点的score=新的score 并且 旧的ele < 新的ele值, 会进入while循环 );
2.调整跳跃表高度
插入节点的高度是随机的,假设要插入节点的高度为3,大于跳跃表的高度2,所以我们需要调整跳跃表的高度。
level = zslRandomLevel();
if (level > zsl->level) {
for (i = zsl->level; i < level; i++) {
rank[i] = 0;
update[i] = zsl->header;
update[i]->level[i].span = zsl->length;
}
zsl->level = level;
}
3.插入节点
当update和rank都赋值且节点已创建好后,便可以插入节点了。
x = zslCreateNode(level,score,ele);
for (i = 0; i < level; i++) {
//更新链表 b->n->f ==> b->z->n->f
x->level[i].forward = update[i]->level[i].forward;
update[i]->level[i].forward = x;
//更新步长
/* update span covered by update[i] as x is inserted here */
x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
update[i]->level[i].span = (rank[0] - rank[i]) + 1;
}
如果新插入节点的高度小于原跳跃表高度,则从level到zsl->level-1层的update[i]节点forward不会指向新插入的节点,所以不用更新update[i]的forward指针,只将这些level层的span加1即可。代码如下。
/* increment span for untouched levels */
for (i = level; i < zsl->level; i++) {
update[i]->level[i].span++;
}
4.调整backward
根据update的赋值过程,新插入节点的前一个节点一定是update[0],由于每个节点的后退指针只有一个,与此节点的层数无关,所以当插入节点不是最后一个节点时,需要更新被插入节点的backward指向update[0]。如果新插入节点是最后一个节点,则需要更新跳跃表的尾节点为新插入节点。插入节点后,更新跳跃表的长度加1。
x->backward = (update[0] == zsl->header) ? NULL : update[0];
if (x->level[0].forward)
x->level[0].forward->backward = x;
else
zsl->tail = x;
zsl->length++;
return x;
3.3.3 删除节点
删除节点的步骤:
1)查找需要更新的节点;2)设置span和forward ;
1.查找需要更新的节点
查找需要更新的节点要借助update数组, ;
2.设置span和forward
删除节点需要设置update数组中每个节点的span和forward。
假设x的第i层的span值为a,update[i]第i层的span值为b,由于删除了一个节点,所以a+b-1的值就是update[i]第i层的span新值。update[i]的第i的新forward就是x节点第i层的forward,这个类似链表删除元素的操作。
如果update[i]第i层的forward不为x,说明update[i]的层高大于x的层高,即update[i]第i层指向了指向了x的后续节点或指向NULL。由于删除了一个节点,所以update[i]的leve[i]的span需要减1。
如果update[i]的forward不为x,在要删除的节点的高度小于跳跃表高度的情况下出现,i大于x高度的节点的forward与x无关,所以这些节点只需更新其span减1即可。
void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update) {
int i;
for (i = 0; i < zsl->level; i++) {
if (update[i]->level[i].forward == x) {
update[i]->level[i].span += x->level[i].span - 1;
update[i]->level[i].forward = x->level[i].forward;
} else {
update[i]->level[i].span -= 1;
}
}
//update节点更新完毕之后,需要更新backward指针、跳跃表高度和长度。
//如果x不为最后一个节点,直接将第0层后一个节点的backward赋值为x的backward即可;
//否则 (x为最后一个节点),将跳跃表的尾指针指向x的backward节点即可。
if (x->level[0].forward) {
x->level[0].forward->backward = x->backward;
} else {
zsl->tail = x->backward;
}
// 当删除的x节点是跳跃表的最高节点,并且没有其他节点与x节点的高度相同时,需要将跳跃表的高度减1
while(zsl->level > 1 && zsl->header->level[zsl->level-1].forward == NULL)
zsl->level--;
//由于删除了一个节点,跳跃表的长度需要减1。
zsl->length--;
}
3.3.4 删除跳跃表
获取到跳跃表对象之后,从头节点的第0层开始,通过forward指针逐步向后遍历, 每个节点被释放之后,释放跳跃表对象,即完成了跳跃表的删除操作。
/* Free a whole skiplist. */
void zslFree(zskiplist *zsl) {
zskiplistNode *node = zsl->header->level[0].forward, *next;
zfree(zsl->header);
while(node) {
//next指向后续节点,释放当前节点后当前node替换为next往后进行释放
next = node->level[0].forward;
zslFreeNode(node);
node = next;
}
zfree(zsl); //释放跳跃表对象
}
3.4 跳跃表的应用
在Redis中,跳跃表主要应用于有序集合的底层实现(有序集合的另一种实现方式为压缩列表)。
Redis的配置文件中关于有序集合底层实现的两个配置。
1)zset-max-ziplist-entries 128:zset采用压缩列表时,元素个数最大值。默认值为128。
2)zset-max-ziplist-value 64:zset采用压缩列表时,每个元素的字符串长度最大值。默认值为64。
zset添加元素的主要逻辑位于t_zset.c的zaddGenericCommand函数中。
zset插入第一个元素时,会判断下面两种条件:
·zset-max-ziplist-entries的值是否等于0;
·zset-max-ziplist-value小于要插入元素的字符串长度。
满足任一条件Redis就会采用跳跃表作为底层实现,否则采用压缩列表作为底层实现方式。
if (server.zset_max_ziplist_entries == 0 ||
server.zset_max_ziplist_value < sdslen(c->argv[scoreidx+1]->ptr))
{
zobj = createZsetObject();//创建跳跃表结构
} else {
zobj = createZsetZiplistObject();//创建压缩列表结构
}
一般情况下,不会将zset-max-ziplist-entries配置成0,元素的字符串长度也不会太长,所以在创建有序集合时,默认使用压缩列表的底层实现。zset新插入元素时,会判断以下两种条件:
·zset中元素个数大于zset_max_ziplist_entries;
·插入元素的字符串长度大于zset_max_ziplist_value。
当满足任一条件时,Redis便会将zset的底层实现由压缩列表转为跳跃表。
zsetAdd函数中:
if (zzlLength(zobj->ptr) > server.zset_max_ziplist_entries ||
sdslen(ele) > server.zset_max_ziplist_value)
zsetConvert(zobj,OBJ_ENCODING_SKIPLIST);
值得注意的是,zset在转为跳跃表之后,即使元素被逐渐删除,也不会重新转为压缩列表。
3.5 本章小结
本章介绍了跳跃表的基本原理和实现过程。跳跃表的原理简单,其查询、插入、删除的平均复杂度都为O(logN)。跳跃表主要应用于有序集合的底层实现。
以上是关于Redis5设计与源码分析 (第3章 跳跃表)的主要内容,如果未能解决你的问题,请参考以下文章
javaweb项目SSM疫情管理系统设计与实现.rar(论文+源码)
课题设计基于nodejs购票系统的设计与实现.zip(论文+源码+ppt文档+视频录制)
Python基于TensorFlow的水果识别软件设计与实现.rar(论文+项目源码+视频演示)
《Redis设计与实现》[第一部分]数据结构与对象-C源码阅读