Redis源码分析(skiplist)
源码版本: redis-4.0.1
源码位置:
一、跳跃表简介
跳跃表(SkipList),其实也是解决查找问题的一种数据结构,但是它既不属于平衡树结构,也不属于Hash结构,它的特点是元素是有序的。有关于跳跃表的更多解释,大家可以参考 张铁蕾老师 - Redis内部数据结构详解(6)——skiplist 中有关跳跃表的描述部分,我接下来主要分析有关于Redis跳跃表本身的代码部分,Redis作者antirez
提到Redis中的实现的跳跃表与一般跳跃表相比具有以下三个特点:
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”. //有一个后退指针,即在第一层实现了一个双向链表,允许后退遍历
接下来我们去看下SkipList的数据结构定义。
二、数据结构定义
有许多数据结构的定义其实是按照(结点+组织方式)来的,结点就是一个数据点,组织方式就是把结点组织起来形成数据结构,比如 双端链表 (ListNode+list)、字典(dictEntry+dictht+dict)等,今天所说的SkipList其实也一样,我们首先看下它的结点
定义:
typedef struct zskiplistNode {
sds ele; //数据域
double score; //分值
struct zskiplistNode *backward; //后向指针,使得跳表第一层组织为双向链表
struct zskiplistLevel { //每一个结点的层级
struct zskiplistNode *forward; //某一层的前向结点
unsigned int span; //某一层距离下一个结点的跨度
} level[]; //level本身是一个柔性数组,最大值为32,由 ZSKIPLIST_MAXLEVEL 定义
} zskiplistNode;
接下来是组织方式,即使用上面的zskiplistNode
组织起一个SkipList:
typedef struct zskiplist {
struct zskiplistNode *header; //头部
struct zskiplistNode *tail; //尾部
unsigned long length; //长度,即一共有多少个元素
int level; //最大层级,即跳表目前的最大层级
} zskiplist;
核心的数据结构就是上面两个。
三、创建、插入、查找、删除、释放
我们以下面这个例子来跟踪SkipList的代码,其中会涉及到的操作有创建、插入、查找、删除、释放
等。(ps:将Redis中main函数的代码替换成下面的代码就可以测试)
// 需要声明下 zslGetElementByRank() 函数,main函数中使用
zkiplistNode* zslGetElementByRank(zskiplist *zsl, unsigned long rank);
int main(int argc, char **argv) {
unsigned long ret;
zskiplistNode *node;
zskiplist *zsl = zslCreate();
zslInsert(zsl, 65.5, sdsnew("tom")); //level = 1
zslInsert(zsl, 87.5, sdsnew("jack")); //level = 4
zslInsert(zsl, 70.0, sdsnew("alice")); //level = 3
zslInsert(zsl, 95.0, sdsnew("tony")); //level = 2
zrangespec spec = { //定义一个区间, 70.0 <= x <= 90.0
.min = 70.0,
.max = 90.0,
.minex = 0,
.maxex = 0};
printf("zslFirstInRange 70.0 <= x <= 90.0, x is:"); // 找到符合区间的最小值
node = zslFirstInRange(zsl, &spec);
printf("%s->%f\n", node->ele, node->score);
printf("zslLastInRange 70.0 <= x <= 90.0, x is:"); // 找到符合区间的最大值
node = zslLastInRange(zsl, &spec);
printf("%s->%f\n", node->ele, node->score);
printf("tony's Ranking is :"); // 根据分数获取排名
ret = zslGetRank(zsl, 95.0, sdsnew("tony"));
printf("%lu\n", ret);
printf("The Rank equal 4 is :"); // 根据排名获取分数
node = zslGetElementByRank(zsl, 4);
printf("%s->%f\n", node->ele, node->score);
ret = zslDelete(zsl, 70.0, sdsnew("alice"), &node); // 删除元素
if (ret == 1) {
printf("Delete node:%s->%f success!\n", node->ele, node->score);
}
zslFree(zsl); // 释放zsl
return 0;
}
Out >
zslFirstInRange 70.0 <= x <= 90.0, x is:alice->70.000000
zslLastInRange 70.0 <= x <= 90.0, x is:jack->87.500000
tony's Ranking is :4
The Rank equal 4 is :tony->95.000000
Delete node:alice->70.000000 success!
- 接下来我们逐行分析代码,首先
zskiplist *zsl = zslCreate();
创建了一个SkipList,需要关注的重点是会初始化zsl->header
为最大层级32,因为 ZSKIPLIST_MAXLEVEL 定义为32,这个原因与SkipList中获取Level的随机函数有关,具体参考文章开头给的博客链接。我们看下zslCreate的代码:
zskiplist *zslCreate(void) {
int j;
zskiplist *zsl;
zsl = zmalloc(sizeof(*zsl)); // 申请空间
zsl->level = 1; // 初始层级定义为1
zsl->length = 0;
zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL); // 初始化header为32层
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; // tail目前为NULL
return zsl;
}
// zslCreateNode根据传入的level和score以及ele创建一个level层的zskiplistNode
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;
}
目前我们的zsl如下图所示:
- 接下来我们开始向zsl中插入数据,
zslInsert(zsl, 65.5, sdsnew("tom"));
zslInsert的代码如下所示:
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
/* 虽然整个代码较长,但是从整体逻辑上可以分为三部分:
* 1:根据目前传入的score找到插入位置x,这个过程会保存各层x的前一个位置节点
* 就像我们对有序单链表插入节点的时候先要找到比目前数字小的节点保存下来。
* 2:根据随机函数获取level,生成新的节点
* 3:修改各个指针的指向,将创建的新节点插入。
*/
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
unsigned int rank[ZSKIPLIST_MAXLEVEL];
int i, level;
/* 第一步: 根据目前传入的score找到插入位置x,并且将各层的前置节点保存至rank[]中 */
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;
}
/* we assume the element is not already inside, since we allow duplicated
* scores, reinserting the same element should never happen since the
* caller of zslInsert() should test in the hash table if the element is
* already inside or not. */
/* 第二步:获取level,生成新的节点 */
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;
}
x = zslCreateNode(level,score,ele);
/* 第三步:修改各个指针的指向,将创建的新节点插入 */
for (i = 0; i < level; i++) {
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;
}
/* increment span for untouched levels */
for (i = level; i < zsl->level; i++) {
update[i]->level[i].span++;
}
/* 更新backword的指向 */
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;
}
需要注意的是span的含义,它表示当前节点距离下一个节点的跨度
,之所以可以根据rank排名获取元素,就是根据span确定的。update[i]保存的就是第 i 层应该插入节点的前一个节点,在第三步更新指针的时候使用。插入了一个元素的zsl如下图所示(level=1):
- 接着我们继续插入后面的三条数据,他们的level分别为
jack->4、alice->3、tony->2
,此时的zsl如下图所示,注意span的更新:
- 好了,插入终于结束啦!接下来我们看下查找的相关操作,上面的代码中有关查找举了4个例子,分别是:
1)查找指定范围内最小的元素
2)查找指定范围内最大的元素
3)根据名称获取排名
4)根据排名获取名称
我们分析下(1)和(4),(2)、(3)同理。首先来看(1),用zrangespec结构体定义了一个范围为70.0 <= x <= 90.0
,有关zrangespec结构体如下所示:
typedef struct {
double min, max; // 定义最小范围和最大范围
int minex, maxex; // 是否包含最小和最大本身,为 0 表示包含,1 表示不包含
} zrangespec;
/* 定义范围的代码如下所示 */
zrangespec spec = { //定义spec, 70.0 <= x <= 90.0
.min = 70.0,
.max = 90.0,
.minex = 0,
.maxex = 0}; //为结构体元素赋值
下面调用zslFirstInRange()
函数遍历得到了满足70.0 <= x <= 90.0
的最小节点,代码如下:
/* Find the first node that is contained in the specified range.
* Returns NULL when no element is contained in the range. */
zskiplistNode *zslFirstInRange(zskiplist *zsl, zrangespec *range) {
zskiplistNode *x;
int i;
/* If everything is out of range, return early. */
if (!zslIsInRange(zsl,range)) return NULL; // 判断给定的范围是否合法
x = zsl->header;
for (i = zsl->level-1; i >= 0; i--) { // 从最高的Level开始
/* Go forward while *OUT* of range. */
while (x->level[i].forward && //只要没结束 && 目前结点的score小于目标score
!zslValueGteMin(x->level[i].forward->score,range))
// 将结点走到当前的节点
x = x->level[i].forward;
}
/* This is an inner range, so the next node cannot be NULL. */
x = x->level[0].forward; // 找到了符合的点
serverAssert(x != NULL);
/* Check if score <= max. */
if (!zslValueLteMax(x->score,range)) return NULL; // 判断返回的值是否小于max值
return x;
}
可以看到,遍历的核心思想是:
(1) 高Level -> 低Level
(2) 小score -> 大score
即在从高Level遍历比较过程中,如果此时的score小于了某个高level的值,就在这个节点前一个节点降低一层Level继续往前遍历,我们找70.0
的路线如下图所示(图中红线):
- 继续看下根据排名获取元素的函数
zslGetElementByRank()
,主要是根据span域来完成,代码如下所示:
zskiplistNode* zslGetElementByRank(zskiplist *zsl, unsigned long rank) {
zskiplistNode *x;
unsigned long traversed = 0;
int i;
x = zsl->header;
for (i = zsl->level-1; i >= 0; i--) {
while (x->level[i].forward && (traversed + x->level[i].span) <= rank)
{
traversed += x->level[i].span;
x = x->level[i].forward;
}
if (traversed == rank) {
return x;
}
}
return NULL;
}
遍历的思想和之前没有什么差别,本次遍历路线如下图所示:
- 接着我们看下
Delete()
函数,ret = zslDelete(zsl, 70.0, sdsnew("alice"), &node);
表示删除zsl中score为70.0,数据为alice的元素,这也是Redis SkipList的第二个特征,比较一个元素不仅比较score,而且比较数据,下面看下zslDelete的代码:
int zslDelete(zskiplist *zsl, double score, sds ele, zskiplistNode **node) {
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
int i;
x = zsl->header;
for (i = zsl->level-1; i >= 0; i--) {
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)))
{
x = x->level[i].forward;
}
update[i] = x;
}
/* We may have multiple elements with the same score, what we need
* is to find the element with both the right score and object. */
x = x->level[0].forward;
if (x && score == x->score && sdscmp(x->ele,ele) == 0) {
zslDeleteNode(zsl, x, update);
if (!node)
zslFreeNode(x);
else
*node = x;
return 1;
}
return 0; /* not found */
}
- 需要注意的是
zslDelete()
第四个参数,是一个zskipListNode **
类型,它如果不为NULL,那么代码在遍历找到node之后不会将其直接释放,而是将地址交给它,后续这块空间的释放就必须由我们手动处理。 - 遍历比较的思想和之前还是一样, 在update[]中记录下各层删除节点之前的节点。
- while循环比较条件,
sdscmp(x->level[i].forward->ele,ele) < 0
是因为插入函数zslInsert()
也是按照这个逻辑插入的。 - 最后需要再次比较
if (x && score == x->score && sdscmp(x->ele,ele) == 0)
是因为Redis SkipList允许相同score的元素存在。
最后看看释放函数zslFree(zsl)
,思想很简单,因为level[0]一定是连续的(并且是一个双向链表),所以从level[0]依次遍历释放就行了。
/* Free a whole skiplist. */
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;
}
zfree(zsl);
}
通过上面的例子,我们分析了zskiplist的创建、插入、查找、删除、释放
等操作,结合数据结构的定义,基本上分析清楚了zskiplist。其实zskiplist在Redis中的主要用处是和dict一起实现Sorted Set
,这个我们后续看Sorted Set的时候再分析。
四、性能分析
操作 | 一般性能 | 最坏性能 |
---|---|---|
插入 | O(log n) | O(n) |
删除 | O(log n) | O(n) |
搜索 | O(log n) | O(n) |
如果想了解更多关于跳表本身,比如RandomLevel()
的随机性等,一定不要错过 维基百科 上的内容。
[完]
Redis源码分析(skiplist)的更多相关文章
- Redis源码分析:serverCron - redis源码笔记
[redis源码分析]http://blog.csdn.net/column/details/redis-source.html Redis源代码重要目录 dict.c:也是很重要的两个文件,主要 ...
- redis源码分析之事务Transaction(下)
接着上一篇,这篇文章分析一下redis事务操作中multi,exec,discard三个核心命令. 原文地址:http://www.jianshu.com/p/e22615586595 看本篇文章前需 ...
- redis源码分析之有序集SortedSet
有序集SortedSet算是redis中一个很有特色的数据结构,通过这篇文章来总结一下这块知识点. 原文地址:http://www.jianshu.com/p/75ca5a359f9f 一.有序集So ...
- Redis源码分析(intset)
源码版本:4.0.1 源码位置: intset.h:数据结构的定义 intset.c:创建.增删等操作实现 1. 整数集合简介 intset是Redis内存数据结构之一,和之前的 sds. skipl ...
- Redis源码分析系列
0.前言 Redis目前热门NoSQL内存数据库,代码量不是很大,本系列是本人阅读Redis源码时记录的笔记,由于时间仓促和水平有限,文中难免会有错误之处,欢迎读者指出,共同学习进步,本文使用的Red ...
- redis源码分析之发布订阅(pub/sub)
redis算是缓存界的老大哥了,最近做的事情对redis依赖较多,使用了里面的发布订阅功能,事务功能以及SortedSet等数据结构,后面准备好好学习总结一下redis的一些知识点. 原文地址:htt ...
- redis源码分析之事务Transaction(上)
这周学习了一下redis事务功能的实现原理,本来是想用一篇文章进行总结的,写完以后发现这块内容比较多,而且多个命令之间又互相依赖,放在一篇文章里一方面篇幅会比较大,另一方面文章组织结构会比较乱,不容易 ...
- Redis源码分析(dict)
源码版本:redis-4.0.1 源码位置: dict.h:dictEntry.dictht.dict等数据结构定义. dict.c:创建.插入.查找等功能实现. 一.dict 简介 dict (di ...
- redis源码分析(一)-sds实现
redis支持多种数据类型,sds(simple dynamic string)是最基本的一种,redis中的字符串类型大多使用sds保存,它支持动态的扩展与压缩,并提供许多工具函数.这篇文章将分析s ...
随机推荐
- ecshop 首页调用指定分类下的销售排行
/*首页调用指定分类下的销售排行*/ function get_cats_top10($cat = '') { $sql = 'SELECT cat_id, cat_name ' . 'FROM ' ...
- Groovy系列(1)- Groovy简述
Groovy简述 前言 由于性能测试的JSR223 Sampler取样器需要用到 Groovy 语言,这两天对其进行了粗略的学习,本文是对学习做的一个简单总结,主要内容参考于官方文档(Groovy 的 ...
- Modern PHP interface 接口
The right way /dev/hell Code Response.php 接口 demo: modern-php/├── data│ └── stream.txt└── interfac ...
- windom 下面redis安装和扩展安装
参考 https://www.cnblogs.com/yulongcode/p/10585229.html https://blog.csdn.net/qq_41921511/article/deta ...
- 鸿蒙内核源码分析(时钟任务篇) | 触发调度谁的贡献最大 | 百篇博客分析OpenHarmony源码 | v3.05
百篇博客系列篇.本篇为: v03.xx 鸿蒙内核源码分析(时钟任务篇) | 触发调度谁的贡献最大 | 51.c.h .o 任务管理相关篇为: v03.xx 鸿蒙内核源码分析(时钟任务篇) | 触发调度 ...
- P5012-水の数列【并查集,RMQ】
正题 题目链接:https://www.luogu.com.cn/problem/P5012 题目大意 \(n\)个数字的一个序列,\(T\)次询问给出\([l,r]\)要求 找出一个最大的\(x\) ...
- 通用JS9
Symbol.toStringTag 该符号作为一个属性表示"一个字符串,该字符串用于创建对象的默认字符串描述."由内置方法Object.prototype.toString()使 ...
- kubelet源码分析——关闭Pod
上一篇说到kublet如何启动一个pod,本篇讲述如何关闭一个Pod,引用一段来自官方文档介绍pod的生命周期的话 你使用 kubectl 工具手动删除某个特定的 Pod,而该 Pod 的体面终止限期 ...
- VUE自学日志01-MVC和MVVM
一.需要了解的基础概念 Model(M)是指数据模型,泛指后端进行的各种业务逻辑处理和数据操控,主要围绕数据库系统展开.这里的难点主要在于需要和前端约定统一的接口规则. View(V)是视图层,也就是 ...
- SpringBoot整个Druid
Druid简介 Java程序很大一部分要操作数据库,为了提高性能操作数据库的时候,又不得不使用数据库连接池. Druid 是阿里巴巴开源平台上一个数据库连接池实现,结合了 C3P0.DBCP 等 ...