redis7.2.2|SkipList源码解析,源码面前了无秘密

本篇文章基于redis7.22
源码位置:redis-7.2.2\src\t_zset.c
有关skiplist的介绍可以参考:skiplist

Zset

Zset是redis五种基本数据类型之一。底层有两种编码"OBJ_ENCODING_SKIPLIST"和"OBJ_ENCODING_ZIPLIST(新版本中ziplis已经被listpack取代)"
在这里插入图片描述

这篇文章只介绍编码为"OBJ_ENCODING_SKIPLIST"的底层实现,即Zset,先来看一下zset的结构

/* ZSETs use a specialized version of Skiplists */
//ZSET使用一种特殊版本的skiplist
typedef struct zset {
   
    dict *dict;
    zskiplist *zsl;
} zset;

为什么zset结构中要包含一个dict?

  • 使用redis命令的时候,可以发现跟zset有关的命令有"zadd key score value"、“zrange key start stop withscores”、“zincrby key field value”…
  • 在获取zset相关信息时只需要输入value的相关值即可并不需要该value对应的score值,但是在实际操作时却需要用到score值,因为zset本身就是按照score值排序的。所以将value到score的映射关系存到了dict,这样可以方便快速的通过value找到与之对应的score值。
  • 哈希表[存储redis object到score之间的映射]另一种是skiplist[存储score到redis object之间的映射]

zset

首先看一下源码中,对于zset的介绍

ZSETs are ordered sets using two data structures to hold the same elements in order to get O(log(N)) INSERT and REMOVE operations into a sorted data structure.

zset是有序集合,使用两种数据结构存储相同的elements,以此保证插入和删除的时间复杂度为O(logn);

The elements are added to a hash table mapping Redis objects to scores.
At the same time the elements are added to a skiplist mapping scores to Redis objects (so objects are sorted by scores in this “view”).

hash table用来存储“redis object”到“score”之间的映射,skiplist存储“score”到"redis object"之间的映射,所以在skiplist中按照score进行排序

Note that the SDS string representing the element is the same in both the hash table and skiplist in order to save memory. What we do in order to manage the shared SDS string more easily is to free the SDS string only in zslFreeNode(). The dictionary has no value free method set.So we should always remove an element from the dictionary, and later from
the skiplist.

skiplist和hash table中的表示元素的sds是同一个,这样做可以节省内存;为了更简单的管理共享的sds字符串,只在zslFreeNode()函数中释放内存;字典没有设置值释放方法,所以应该先从字典中删除元素,再从skiplist中删除元素。

This skiplist implementation is almost a C translation of the original algorithm described by William Pugh in “Skip Lists: A Probabilistic Alternative to Balanced Trees”, modified in three ways:
a) this implementation allows for repeated scores.
b) the comparison is not just by key (our ‘score’) but by satellite data.
c) there is a back pointer, so it’s a doubly linked list with the back pointers being only at “level 1”. This allows to traverse the list from tail to head, useful for ZREVRANGE.

这个跳表的实现,几乎是对“William Pugh 在 “Skip Lists: A Probabilistic Alternative to Balanced Trees”,中描述的原始算法的C语言翻译,并作出了以下三个修改
1.skiplist允许插入重复的score
2.skiplist,排序的第一关键字是"score",第二关键字是“satellite data”也就是原始数据value值
skiplist的实现中包含一个backward后退指针(只有最后一层才有),为了能够实现从后往前遍历,对应的命令是"zrevrange"

Data_Struct

zskiplist

看"zskiplist"的结构之前,先回顾一下"single LinkedList单链表"的结构

typedef ListNode struct{
   
	int val;
	struct ListNode *next;
}ListNode;

typedef SingleList struct{
   
	struct ListNode *head;
	struct ListNode *tail;
}SingleList;

其实"zskiplist"的本质就是链表

zskiplist

typedef struct zskiplist {
   
    struct zskiplistNode *header, *tail;
    unsigned long length;//表示元素的个数,所以跳表获取元素个数的时间复杂度为O(1),等下我们再从源码中进行验证
    int level;//表示跳表的层数
} zskiplist;
  • 通过源码可以发现,zskiplist就是由zkiplistNode组成的包含有头尾指针的一个链表,并且还存储这个链表中的元素个数以及当前的总层数。

zskiplistNode

typedef char *sds;
typedef struct zskiplistNode {
   
	//命令[zadd key score member [score member..]]
	//存储命令中的member值,
    sds ele;
    //存储命令中的score值
    double score;
    //最后一层的后退指针,为了实现从后往前遍历,对应命令zrevrange
    struct zskiplistNode *backward;
    //下面详细解释
    struct zskiplistLevel {
   
        struct zskiplistNode *forward;
        unsigned long span;
    } level[];
} zskiplistNode;

我们单独看一下zskiplistNode中的“level数组”是什么;
拆开来看,先看最里面的结构,

struct zskiplistNode * forward;
//这个结构是不是很类似于单链表中的next指针
//struct ListNode *next;

然后看外面一层的结构

struct zskiplistLevel {
   
        struct zskiplistNode *forward;
    };
//定义了一个结构体类型zskiplistLevel,这个结构体中包含一个字段zskiplistNode*类型的forward指针,相当于对单链表的next指针做了一层包装,将其放入一个结构体中。

接着往下看

struct zskiplistLevel {
   
        struct zskiplistNode *forward;
    } level[];
//用刚刚定义好的结构体定义了zskiplistLevel类型的数组level[也即是定义结构体的同时定义变量]。
//跳表相对于单链表来说多了好几层,每一层都相当于一个单链表[最底层相当于一个双向链表],所以每一层都需要一个类似next指针的forward指针

如果你还不明白,再解释的仔细一点,来看看C语言中结构体定义变量的方式

//1.结构体变量的定义,放在结构体声明之后
struct student{
   
	int id;
	char sex;
	char name[23];
};
struct student stu[];
//2.结构体声明的同时定义变量,
struct student{
   
	int id;
	char sex;
	char name[23];
}stu[];
//3.匿名方式定义变量,但是只能定义一次
struct {
   
	int id;
	char sex;
	char name[23];
}stu[];
//定义数组的时候可以指定长度也可以不指定长度
int arr[3];
int arr[];//两种方式都是可以的

好的,接下来继续看看zskiplistNode里面还有什么

struct zskiplistLevel {
   
        struct zskiplistNode *forward;
        unsigned long span;
    } level[];

还有zskiplistLevel中的span没有介绍
先透露一下,这个span是用来记录当前节点到下一个节点经历的跨度,有了这个字段可以很快的得出当前节点的排名,怎么做到的呢?就是在寻找目标节点的过程中,将遇到节点的span加起来就是目标节点的rank,对应的命令就是[zrank key member ].

为了加深对"zskiplist"结构的理解,我画了一张示意图,如下所以
在这里插入图片描述

zskiplist结构示意图

有关skiplist的结构定义就介绍完了接下来介绍有关skiplist的一些函数。介绍相关函数之前再来总结一下skiplist的结构:
其实跳表的本质,是一个拥有多层,每一层都是一个单链表,最底层是一个双链表的链表结构

Function

逐个分析有关跳表的各个函数

zslCreateNode

//利用所给的节点的level,以及score值和ele值[也就是对应命令中的member]创建一个新的节点
zskiplistNode *zslCreateNode(int level, double score, sds ele) {
   
	//分配内存,这里分配了两部分的内存,一部分是节点本身的内存,也就是为ele,score,backward
	//另一部分是为forward和span也就是level数组分配内存
    zskiplistNode *zn =
        zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel));
    zn->score = score;
    zn->ele = ele;
    return zn;
}

zslCreate

创建一个新的skiplist

#define ZSKIPLIST_MAXLEVEL 32 
#define ZSKIPLIST_P 0.25  
zskiplist *zslCreate(void) {
   
    int j;
    zskiplist *zsl;
	//为zskiplist本身的结构分配内存
    zsl = zmalloc(sizeof(*zsl));
    //将skiplist的层数设置为1
    zsl->level = 1;
    //元素个数设置为0
    zsl->length = 0;
    //初始化头结点,ZSKIPLIST_MAXLEVEL==32
    zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL);
    //初始化level中的forward
    for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) {
   
        zsl->header->level[j].forward = NULL;
        zsl->header->level[j].span = 0;
    }
    //初始化头节点的backward指针
    zsl->header->backward = NULL;
    //设置尾节点为null
    zsl->tail = NULL;
    return zsl;
}

zslFreeNode

//释放zskiplistNode节点

void zslFreeNode(zskiplistNode *node) {
   
	//先释放sds指向的字符串
    sdsfree(node->ele);
    //释放node本身
    zfree(node);
}

zslFree

//释放zskiplist

void zslFree(zskiplist *zsl) {
   
    zskiplistNode *node = zsl->header->level[0].forward, *next;
	//释放头节点
    zfree(zsl->header);
    //依次释放每个节点
    while(node) {
   
        next = node->level[0].forward;
        zslFreeNode(node);
        node = next;
    }
    //释放zslskiplist
    zfree(zsl);
}

这里解释一下,为什么只是释放,“node->level[0].forward”而不是遍历每一层,进行释放;因为最底层也就是0层包含所有的节点,我们在插入一个节点的时候,将其插入在了第0层,上层只是拥有该节点的相应层的forward指针。

zslRandomLevel

//为即将插入的节点生成一个随机level,表示该节点所拥有的层数
//level处于[1,ZSKIPLIST_MAXLEVEL(32)]之间
//类似于幂律分布,不会产生太大level,这样一来所有节点的层数都能控制在一个相对平衡的状态
int zslRandomLevel(void) {
   
	//
    static const int threshold = ZSKIPLIST_P*RAND_MAX;
    int level = 1;
    while (random() < threshold)
        level += 1;
    return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}

这是在redis7.2.2版本内的代码,但是我没有找到“RAND_MAX”,我们来看一下redis_3.2.100版本的代码

/*
	random()返回的是一个随机数
	0xFFFF转换为二进制是“1111 1111 1111 1111”十进制是65535
	random()&0xFFFF可以将随机数的高位清0,只留下低16位,也就是生成了一个16位的随机数
	(random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF)这行代码的意思就是:
	生成的一个16位的随机数小于(ZSKIPLIST_P * 0xFFFF)的概率,16位随机数的取值在[1,0xFFFF], (ZSKIPLIST_P * 0xFFFF)的取值在[1,(ZSKIPLIST_P * 0xFFFF)],用古典概率计算,
	p=(ZSKIPLIST_P * 0xFFFF)/0xFFFF == ZSKIPLIST_P 
	
	感叹一下这个算法真的巧妙
*/
int zslRandomLevel(void) {
   
    int level = 1;
    while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
        level += 1;
    return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}

随机算法是一个概率算法,很大程度上能保证skiplist处于一个相对平衡的状态,但是在极端情况下仍然可能退化为单链表。
随机算法是skiplist实现的关键,在插入一个新的节点的之前,需要先调用随机函数生成新节点所在的level,然后根据level创建新的节点,之后寻找插入位置并插入。
一个节点的level在插入之前就已经决定好了,被删除之前都不会发生变化。

zslInsert

//向skiplist中插入一个newnode
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
   
	//记录每一层插入位置的前一个节点
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    
    //暂时还不知道这个rank记录的是什么,我们先向下看
    unsigned long rank[ZSKIPLIST_MAXLEVEL];
    
    int i, level;
	//检查score是否合法,如果score not a number则会引发错误,程序停止运行
    serverAssert(!isnan(score));
    //从头结点出发
    x = zsl->header;
    //从最高层开始向下寻找
    for (i = zsl->level-1; i >= 0; i--) {
   
        //初始化rank,如果i指向最高一层,则将rank初始化为0
        //i指向非最高层初始化为上一层的rank;仔细一想会发现所有的层rank其实都初始化为了0,rank[i]=0
        rank[i] = i == (zsl->level-1) ? 0 : rank[i+
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值