Redis数据结构之跳表

跳表是一种有序的数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。其核心思想就是通过建立多级索引来实现空间换时间。
在Redis中,使用跳表作为Zset的一种底层实现之一,这也是跳表在Redis中的唯一使用场景。

跳表的实现

跳表由zskiplistNode和zskiplist两个结构定义。其中zskiplistNode表示跳跃表的节点,zskiplist则表示跳跃表节点的相关信息。
跳表

zskiplistNode

typedef struct zskiplistNode {
    sds ele; // 元素值
    double score; // 分值
    struct zskiplistNode *backward; // 后退指针
    struct zskiplistLevel { // 各层信息
        struct zskiplistNode *forward; // 该层前向指针
        unsigned long span; // 该层的跨度
    } level[];
} zskiplistNode;

前进节点

每个层都有一个指向表尾方向的前进指针,用于从表头向表尾方向访问节点。

跨度

记录两个节点之间的距离。跨度是用来计算rank的,在查找某个节点的过程中,将沿途访问过的所有层的跨度累加起来,得到的结果就是目标节点在跳表中的rank。

后退指针

用于表示表尾向表头方向的访问节点,后退节点每次只能后退至前一个节点。

zskiplistList

typedef struct zskiplist {
    struct zskiplistNode *header, *tail; // 头、尾指针
    unsigned long length; // 跳表长度
    int level; // 跳表层数
} zskiplist;

header

指向跳跃表的表头节点。

tail

指向跳跃表的表尾节点。

level

记录当前跳表的长度,即跳表包含节点的数量。

跳表的操作

新建跳表

/* Create a new skiplist. */
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;
}
  • 首先分配内存
  • level设置为1,length设置为0,后退指针设置为null,尾节点设置为null
  • 头指针指向一个高度为32的节点
  • 为头指针的前进节点设置为null,span设置为0

插入节点

/* Insert a new node in the skiplist. Assumes the element does not already
 * exist (up to the caller to enforce that). The skiplist takes ownership
 * of the passed SDS string 'ele'. */
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x; // update记录每层的应该指向新增节点的节点
    unsigned long rank[ZSKIPLIST_MAXLEVEL]; // rank记录每层需要更新的span值
    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]; // 最高层rank为0,非最高层rank初始化为上一层的rank值
        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 = zslRandomLevel(); // 根据幂次定律生成响应的层数
    if (level > zsl->level) { // 对于高出现有的层数,依次遍历,更新rank、后置节点和跨度
        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; // 针对每一层实现节点的插入。新插入的节点x的forward指向update
        update[i]->level[i].forward = x; // update的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]); // 对x节点更新span
        update[i]->level[i].span = (rank[0] - rank[i]) + 1; // 对update节点更新span
    }

    /* increment span for untouched levels */
    for (i = level; i < zsl->level; i++) { // 所有高出的层级更新span++
        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的尾节点
    zsl->length++; // 更新zsl的长度
    return x;
}

更新节点分值

/* Update the score of an element inside the sorted set skiplist.
 * Note that the element must exist and must match 'score'.
 * This function does not update the score in the hash table side, the
 * caller should take care of it.
 *
 * Note that this function attempts to just update the node, in case after
 * the score update, the node would be exactly at the same position.
 * Otherwise the skiplist is modified by removing and re-adding a new
 * element, which is more costly.
 *
 * The function returns the updated element skiplist node pointer. */
zskiplistNode *zslUpdateScore(zskiplist *zsl, double curscore, sds ele, double newscore) {
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x; // 记录需要更新节点在每一层的位置
    int i;

    /* We need to seek to element to update to start: this is useful anyway,
     * we'll have to update or remove it. */
    x = zsl->header;
    for (i = zsl->level-1; i >= 0; i--) {
        while (x->level[i].forward &&
                (x->level[i].forward->score < curscore ||
                    (x->level[i].forward->score == curscore &&
                     sdscmp(x->level[i].forward->ele,ele) < 0)))
        {
            x = x->level[i].forward;
        }
        update[i] = x;
    }

    /* Jump to our element: note that this function assumes that the
     * element with the matching score exists. */
    x = x->level[0].forward;
    serverAssert(x && curscore == x->score && sdscmp(x->ele,ele) == 0);

    /* If the node, after the score update, would be still exactly
     * at the same position, we can just update the score without
     * actually removing and re-inserting the element in the skiplist. */
    if ((x->backward == NULL || x->backward->score < newscore) &&
        (x->level[0].forward == NULL || x->level[0].forward->score > newscore))
    { // 如果针对最后一个节点的更新分值变大或者对第一个节点的更新分值减小,可以直接更新分值即可,无需移动节点
        x->score = newscore;
        return x;
    }

    /* No way to reuse the old node: we need to remove and insert a new
     * one at a different place. */
    zslDeleteNode(zsl, x, update); // 首先将旧分值节点删除
    zskiplistNode *newnode = zslInsert(zsl,newscore,x->ele); // 为新分值新建一个新的节点,并插入
    /* We reused the old node x->ele SDS string, free the node now
     * since zslInsert created a new one. */
    x->ele = NULL;
    zslFreeNode(x);
    return newnode;
}

根据排名获取节点

/* Finds an element by its rank from start node. The rank argument needs to be 1-based. */
zskiplistNode *zslGetElementByRankFromNode(zskiplistNode *start_node, int start_level, unsigned long rank) {
    zskiplistNode *x;
    unsigned long traversed = 0;
    int i;

    x = start_node;
    for (i = start_level; i >= 0; i--) { // 从最高层开始查找,如果上层找到直接return,否则进入下一层
        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; // 最终不存在
}

删除节点

/* Internal function used by zslDelete, zslDeleteRangeByScore and
 * zslDeleteRangeByRank. */
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;
        }
    }
    if (x->level[0].forward) {
        x->level[0].forward->backward = x->backward;
    } else {
        zsl->tail = x->backward;
    }
    while(zsl->level > 1 && zsl->header->level[zsl->level-1].forward == NULL)
        zsl->level--;
    zsl->length--;
}

Zset获取元素的分值

Zset跳表和字典
Zset除了使用zskiplist来实现之外,结构中还使用字典为有序集合创建了一个成员到分值的映射。字典中的每个键值对都保存了一个集合元素,其中键保存了元素的成员,值保存了元素的分值。通过字典可以O(1)的实现查看某个元素的分值。zscore命令就是根据这一特性实现的。
另外需要主要的是,虽然zset结构同时使用跳表和字典来保存有序集合元素,但是两种结构通过指针共享相同元素的成员和分值,所以同时使用跳表和字典来保存集合元素不会产生任何重复成员或者分值,也不会因此而浪费额外的内存。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/211125.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

IO延迟引起的虚拟机故障排查

vmware 虚拟机连上之后总感觉非常卡&#xff0c;查看CPU 内存资源使用率是正常的。 message 日志有cpu卡住的报错 NMI watchdog: BUG: soft lockup - CPU#8 stuck for 23s! [container-31451:45878]下面分析是什么导致的服务器cpu卡住。 1、打开prometheus&#xff0c;观察服务…

CSS新手入门笔记整理:CSS图片样式

图片大小 语法 width:像素值; height:像素值; 图片边框&#xff1a;border 语法 边框&#xff1a;宽度值 样式值 颜色值&#xff1b; border:1px solid red; 图片对齐 水平对齐&#xff1a;text-align 语法 text-align:取值; 属性值 说明 left 左对齐(默认值) cent…

神经网络 代价函数

神经网络 代价函数 首先引入一些便于稍后讨论的新标记方法&#xff1a; 假设神经网络的训练样本有 m m m个&#xff0c;每个包含一组输入 x x x和一组输出信号 y y y&#xff0c; L L L表示神经网络层数&#xff0c; S I S_I SI​表示每层的neuron个数( S l S_l Sl​表示输出…

[英语学习][5][Word Power Made Easy]的精读与翻译优化

[序言] 今日完成第18页的阅读, 发现大量的翻译错误以及不准确. 需要分两篇文章进行讲解. [英文学习的目标] 提升自身的英语水平, 对日后编程技能的提升有很大帮助. 希望大家这次能学到东西, 同时加入我的社区讨论与交流英语相关的内容. [原著英文与翻译版对照][第18页] Wh…

基于SpringBoot的企业客户管理系统的设计与实现

摘 要 本论文主要论述了如何使用JAVA语言开发一个企业客户管理系统&#xff0c;本系统将严格按照软件开发流程进行各个阶段的工作&#xff0c;采用B/S架构&#xff0c;面向对象编程思想进行项目开发。在引言中&#xff0c;作者将论述企业客户管理系统的当前背景以及系统开发的目…

STM32---MDK工程创建

本节我们带领大家学习如何新建一个寄存器库版本MDK的详细步骤&#xff1b; 由于51单片机的学习时&#xff0c;所涉及的寄存器很少&#xff0c;所以往往几个头文件、驱动文件就可以完成相关的功能&#xff0c;但是对于STM32来讲&#xff0c;涉及的寄存器、头文件等都很多&#…

CleanMyMac X4.16.2最新2024注册许可证

都说苹果的闪存是金子做的&#xff0c;这句话并非空穴来风&#xff0c;普遍都是256G起步&#xff0c;闪存没升级一个等级&#xff0c;价格都要增加上千元。昂贵的价格让多数消费者都只能选择低容量版本的mac。而低容量的mac是很难满足用户的需求的&#xff0c;伴随着时间的推移…

005、简单页面-容器组件

之——布局 目录 之——布局 杂谈 正文 1.布局基础知识 2.Column 3.Row 4.实践 杂谈 布局容器组件。 一个丰富的页面需要很多组件组成&#xff0c;那么&#xff0c;我们如何才能让这些组件有条不紊地在页面上布局呢&#xff1f;这就需要借助容器组件来实现。 容器组件是…

elment Loading 加载组件动态变更 text 值bug记录

先上效果图: 倒计时4分钟组件方法 // 倒计时 4分钟getSencond() {this.countDown 4分00秒this.interval setInterval(() > {this.maxTime--;let minutes Math.floor(this.maxTime / 60);let seconds Math.floor(this.maxTime % 60);minutes minutes < 10 ? 0 minu…

AI 绘画 | Stable Diffusion 电商模特

前言 上一篇文章讲到如何给人物更换背景和服装。今天主要讲电商模特,就是服装电商们的固定服装产品图片如何变成真人模特穿上的固定服装产品效果图。如果你掌握了 《AI 绘画 | Stable Diffusion 人物 换背景|换服装》,这篇文章对你来说,上手会更轻松。 教程 提取服装蒙版…

解决Wireshark分析RTMP抓包时Unknown问题

使用Wireshark抓包时&#xff0c;经常出现很多Unknown包&#xff0c;但实际上的字节流实际是正常的。 其实&#xff0c;RTMPT设置里有一个最大包大小的设置&#xff0c;默认是32768&#xff0c;而且默认RTMPT协议配置了从多个TCP流中重组RTMPT的功能(应当是考虑基于HTTP的传输…

SpringBoot+SSM项目实战 苍穹外卖(2)

继续上一节的内容&#xff0c;本节完成新增员工、员工分页查询、启用禁用员工账号、编辑员工、导入分类模块功能代码。 目录 新增员工(完整流程分为以下五个部分)需求分析和设计代码开发功能测试代码完善 (ThreadLocal 线程局部变量)代码提交 员工分页查询代码完善 扩展Spring …

css中元素水平居中的方式

文章目录 前言水平居中&#xff1a;垂直居中方法一: text-align: centerdisplay: table-cell方法二:父元素静态定位子元素通过相对定位来实现方法三:通过静态和相对定位方法四 css图片居中用text-align:center无效怎么回事&#xff1f;如何让图片在DIV中水平和垂直两个方向都居…

prometheus|云原生|kubernetes内部安装prometheus

架构说明&#xff1a; prometheus是云原生系统内的事实上的监控标准&#xff0c;而kubernetes集群内部自然还是需要就地取材的部署prometheus服务了 那么&#xff0c;prometheus-server部署的方式其实是非常多的&#xff0c;比如&#xff0c;kubesphere集成方式&#xff0c;h…

前端面试JS— JS数据类型及相关内容

目录 JS数据类型 基本数据类型&#xff1a; 引用数据类型&#xff1a; 两种数据存储方式&#xff1a; 两种数据类型的区别&#xff1a; 数据类型的检测方式 null和undefined区别 JS数据类型 基本数据类型&#xff1a; Number&#xff0c;String&#xff0c;Boolean&#xff0c;…

Safe and Practical GPU Computation in TrustZone论文阅读笔记

Safe and Practical GPU Computation in TrustZone 背景知识&#xff1a; youtube GR视频讲解链接&#xff1a;ASPLOS’22 - Session 2A - GPUReplay: A 50-KB GPU Stack for Client ML - YouTube GPU软件栈&#xff1a; 概念&#xff1a;"GPU软件栈"指的是与GPU硬件…

以下哪项不是在内蒙古举办的?()

需要查看更多试题和答案&#xff0c;可以前往题海舟&#xff08;题海舟&#xff09;进行搜题查看。可以搜“题干关键词”。 以下哪项不是在内蒙古举办的&#xff1f;&#xff08;&#xff09; A.中蒙博览会 B.东北亚区域合作高峰论坛 C.中蒙俄合作高层论坛 D.中日经济合作会 …

C语言之位段(详解)

C语言之位段 文章目录 C语言之位段1. 位段的介绍2. 位段的内存分配3. 位段跨平台问题4. 位段的应用5. 位段使用注意 1. 位段的介绍 位段&#xff08;bit-field&#xff09;是C语言中的一种特殊数据类型&#xff0c;它允许将一个字节分成几个部分&#xff0c;并为每个部分指定特…

Sql Server数据库跨机器完整恢复(源文件恢复)

问题描述 在操作系统异常的情况下&#xff0c;SQL Server 和相关的业务系统遭受了不可用的情况。由于操作系统问题&#xff0c;导致旧服务器无法正常运行。为了恢复业务功能并确保数据完整性&#xff0c;采取了以下步骤来在新机器上进行 SQL Server 的重新安装和数据恢复。 面…

C#网络编程(System.Net命名空间和System.Net.Sockets命名空间)

目录 一、System.Net命名空间 1.Dns类 &#xff08;1&#xff09;示例源码 &#xff08;2&#xff09;生成效果 2.IPAddress类 &#xff08;1&#xff09;示例源码 &#xff08;2&#xff09;生成效果 3.IPEndPoint类 &#xff08;1&#xff09; 示例源码 &#xff0…