当前位置:网站首页>redis 源码分析 跳表实现
redis 源码分析 跳表实现
2022-07-17 05:08:00 【HjasnJH】
redis中为什么要使用跳表的数据结构?
reids 的zset有序集合的实现,需要兼顾方便的查找,插入和删除,如果使用链表,查找效率位O(N),这对redis不可接受,使用红黑树,则实现比较麻烦,所以redis使用了一种新的结构,跳表。
上图是一个理想跳表
如果你现在要查找21,你只需要从最高的第2层开始,找到18,跳到第1层,找到24,跳到第0层,找到21,如果数据量大,这将达到了二分查找的效果。
从上面理想跳表,我们可以思考下如果定义一个跳表的数据结构,应该具备什么?
如果是一个跳表的节点:比如1这个节点
跳表的粗略的数据结构
struct MySkipListLevel
{
TMySkipListNode* m_pNext; // 下一个节点
int m_nSpan; // 当前层跳离下一节点的间隔
};
struct TMySkipListNode
{
void* m_pData; // 数据
MySkipListLevel m_L[]; // 每一层
TMySkipListNode* m_pPrev; // 查找到24,要往回走
};
struct TMySkipList
{
TMySkipListNode* m_pHeadNode;
int m_nLeves;
};
reids跳表
typedef struct zskiplistNode {
sds ele;
double score;
struct zskiplistNode* backward; // 后退指针, 只能指向当前节点最底层的前一个节点,
struct zskiplistLevel {
struct zskiplistNode *forward; // 指向后一个节点
unsigned long span; // forward指向的节点与本节点之间的元素个数
} level[];
} zskiplistNode;
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
其中,多了尾部指针tail,length,一会看看有什么用,数据部分的差异忽略不计。
redis实现的跳跃表
1、创建跳跃表节点
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;
}
2、创建跳跃表并创建头节点
新跳跃表的默认层高是多少?
int zslRandomLevel(void) {
int level = 1;
while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
level += 1;
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
从代码可以看出,假设小于(ZSKIPLIST_P * 0xFFFF) 的概率为p
大于(ZSKIPLIST_P * 0xFFFF)的概率为1-p
则层数为n,对应的(p^(n-1)) * (1-p)
其数学期望为:E=1/(1-p),当p为0.25时,则对应的E = 1.33
创建跳跃表,头节点不存储信息,层数64,无数据。
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、插入节点
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
unsigned int rank[ZSKIPLIST_MAXLEVEL];
int i, level;
serverAssert(!isnan(score));
x = zsl->header;
// 从最高层往底层访问
// rank[i]是第i层对应的可插入新节点的前一个节点的经过的span(也就是头节点到插入节点的前一个节点的间隔),最高层为0
// update[i] 是第i层可以插入节点的前一个节点
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的节点,如果比原来的调表的高度高,需要更新对应的rank和update
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 */
// 插入新的节点后,对应的高出来的那几层需要更新span
for (i = level; i < zsl->level; i++) {
update[i]->level[i].span++;
}
// 设置后向节点上数值
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;
}
插入跳表的节点,1、关键在于如果低于层数,则高于当前跳表层数的要赋值对应的层的数据。2、维持好插入节点的前后节点。3、维持好插入节点后整个跳表的其他节点,长度,层高的影响。
4、删除节点:
void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update) {
int i;
// 下一个节点就是被删除节点,则加上被删除节点的间距-1,不是被删除节点,直接-1
// 这里假定,被删除的节点一定存在跳表中
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;
}
}
// 如果被删除节点有下一个节点,则要将下一个节点前向指针赋值为被删除节点的上一个节点
if (x->level[0].forward) {
x->level[0].forward->backward = x->backward;
} else {
zsl->tail = x->backward; // 更新尾节点
}
// 跳表高度大于1,最高层没有下一个节点,层数减1,调整高度
while(zsl->level > 1 && zsl->header->level[zsl->level-1].forward == NULL)
zsl->level--;
// 更新长度
zsl->length--;
}
5、删除跳跃表:
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);
}
从最底层遍历,然后往后遍历,释放他的所有的节点。
跳表redis的应用:
redis中,zset有两种实现,跳表和压缩列表
当元素个数小于128时,使用压缩列表
元素个数大于128时,使用跳表
所以zset当满足条件时,会转换为跳表实现,而且不会转回来。
边栏推荐
猜你喜欢
随机推荐
父组件加scoped有时也会影响子组件
百度地图 实现 热力图
【Es6】详细解说Set ,Array的常用对象及其他方法(完整版)
Ucharts chart, pie chart, bar chart and line chart are used in uniapp
Es6 真实案例解构(多维数组对象)全新案例:
轮播图移动速度(匀速,缓动)案例归总
mysql的锁
mysql的缓存方案问题解决
cookie是否有效时间限定?如何设置cookie?手把手教你设置
遍历的方法总结
Solve the problem of inconsistent prediction effect between text detection training model and information model based on paddleocr
Wechat applet obtains the week, morning, noon and evening of month, year and day
markdown笔记以及Typora相关快捷键
ES6 latest commonly used knowledge dictionary (which can help you solve interview questions, problems in programming, etc.)
Cesium geojson数据的添加与移除
实习项目2-主页配置-我的数据模块
2020-10-22
vlookup函数的使用方法及实例
Excel template export of easypoi
Installation and fast use of Mongo DB stand-alone version