深入解读redis的zset和跳表【源码分析】

news2024/11/16 5:51:22

1.基本指令

部分指令,涉及到第4章的api,没有具体看实现,但是逻辑应该差不多。

  • zadd <key><score1><value1><score2><value2>...
    • 将一个或多个member元素及其score值加入到有序集key当中。
    • 根据zslInsert
  • zrange <key><start><stop>[WITHSCORES]
    • 返回有序集key中,下标在 之间的元素
    • 根据zslGetElementByRank以及backward指针
  • zrangebyscore key min max [withscores] [limit offset count]
    • 返回有序集 key 中,所有score值介于min和max 之间(包括等于min或max )的成员
    • 根据zslFirstInRangezslLastInRange以及backward指针
  • zrank <key><value>
    • 返回该值在集合中的排名,从0开始。
    • 根据zslGetRank

2.数据结构

ZSET是由有序集合跳表实现的,按照分值的大小排序,分值相同时,按照成员对象的大小进行排序。同一个跳表可以有同分值的节点,但是对象必须是唯一的。
在这里插入图片描述
定义结构的代码src/server.h

// 1.ZSET节点
/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
	// member元素的value
    sds ele;
    // member元素的score
    double score;
    // 后向指针
    struct zskiplistNode *backward;
    // 层
    struct zskiplistLevel {
        // 前进指针
        struct zskiplistNode *forward;
        // 跨度
        unsigned long span;
    } level[];
} zskiplistNode;

// 2.ZSET链表
typedef struct zskiplist {
	// 头节点和尾节点
    struct zskiplistNode *header, *tail;
    // 节点的数量(不包括头节点)
    unsigned long length;
    // 表中层数最大节点的层数
    int level;
} zskiplist;

结合上方的图容易理解,其中有一些值得注意的点

  1. header表头节点只有level,没有存放元素的value和score。在zskiplist的length也不包括头节点。
  2. 每一层都有两个属性:前向forward指针和跨度。前向指针指向包含同一层的下一个结点,跨度记录了两个节点间的距离。指向NULL的跨度都为0。跨度是用来计算排位rank的,在查找某个节点的过程中,将沿途访问过的所有层跨度累计起来,就能得到目标节点的排位。
  3. 后向backward指针指向当前节点的前一个节点。目的是遍历。和range有关的指令,可以获得range范围的首尾节点后,从尾节点遍历到首节点。(只有backward指针是遍历相邻节点,forward指针每一层都有,指向的间隔为span的节点,不是下一个节点)
  4. 每次创建一个跳表节点时,根据幂次定律随机生成一个介于1到32之间的值作为level数组的大小。(见第3章节复杂度分析)
  5. 节点的score是一个double类型的浮点数,成员对象value是一个SDS(字符串对象)。如果想用zset实现两个维度排序,可以用拼接的思想。

3.跳表通用复杂度分析

跳表的复杂度和level的层数有关,如果只有一层,那复杂度必然都是最坏情况O(N)。一个节点有多少层来自于下面这个函数,在新建节点时,根据幂次定律生成一个1到32间的随机数。
可以理解为有概率P多加一层。

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;
}

我们知道完全二叉树的复杂度推导是
2 h − 1 = N 2^h-1=N 2h1=N
h = l o g 2 ( N + 1 ) h=log_2(N+1) h=log2(N+1)所以平均查找的时间复杂度是O(log(N))
跳表相当于一个多叉树,叉为 1 P \frac{1}{P} P1。(每一个节点有P的概率加一层,那相邻两层的节点数比为P。由于跳表最多32层,相邻两层实际节点数也不严格为P,所以这是一个近似的概念。)
复杂度推导为
( 1 P ) h − 1 − 1 = N (\frac{1}{P})^{h-1}-1=N (P1)h11=N
h = l o g 1 p ( N + 1 ) h=log_{\frac{1}{p}}(N+1) h=logp1(N+1)
如果p=0.25, h = 0.5 ∗ l o g 2 ( N + 1 ) h=0.5*log_2(N+1) h=0.5log2(N+1)
如果p=0.5, h = l o g 2 ( N + 1 ) h=log_2(N+1) h=log2(N+1)
所以p在一定范围都是O(log(N))级别的复杂度。P在极端情况下(比如接近0或1)会变成O(N)。

推导比较粗糙,可能有问题

4.API复杂度分析

4.1. 查找元素

zslFirstInRange找到分值范围的第一个元素;zslLastInRange找到分值范围的最后一个元素
平均O(logN),最坏O(N)

zskiplistNode *zslFirstInRange(zskiplist *zsl, zrangespec *range) {
    zskiplistNode *x;
    int i;
    /* 判断跳表分数的范围是否在该范围内 */
    if (!zslIsInRange(zsl,range)) return NULL;
    x = zsl->header;
    /** 从最高的层数开始遍历,直到最底层 **/
    for (i = zsl->level-1; i >= 0; i--) {
        /* 在同一层通过前向指针遍历,直到下一个节点为空或者下一节点分数大于等于范围最小值,进入下一层 */
        while (x->level[i].forward &&
            !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;
    return x;
}
int zslValueGteMin(double value, zrangespec *spec) {
    return spec->minex ? (value > spec->min) : (value >= spec->min);
}

4.2. 判断分值是否在范围

zslIsInRange判断是否至少一个节点的分值在范围内
O(1),根据头尾节点实现。zslFirstInRangezslLastInRange都会先调用这个函数进行判断。

/* 存在返回1,不存在返回0 */
int zslIsInRange(zskiplist *zsl, zrangespec *range) {
    zskiplistNode *x;
    /* 对值的范围进行判定 */
    if (range->min > range->max ||
            (range->min == range->max && (range->minex || range->maxex)))
        return 0;
    // 1.获取尾节点,尾节点的分数不大于等于(就是小于)范围的最小值返回0
    x = zsl->tail;
    if (x == NULL || !zslValueGteMin(x->score,range))
        return 0;
    // 2.获取头节点,头节点的分数大于范围的最大值返回0
    x = zsl->header->level[0].forward;
    if (x == NULL || !zslValueLteMax(x->score,range))
        return 0;
    return 1;
}

4.3. 添加元素

zslInsert添加元素
平均O(logN),最坏O(N)

zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
	/* 为了插入节点到正确位置,存储遍历过程中每一层最尽头的节点,其实就是新节点的上一个节点(该节点的forward指向新节点)*/
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    /* 为了更新span,存储遍历过程中每一层的rank */
    unsigned long rank[ZSKIPLIST_MAXLEVEL];
    int i, level;
	
    serverAssert(!isnan(score));
    /**和查找类似的思路**/
    x = zsl->header;
    for (i = zsl->level-1; i >= 0; i--) {
        /* 存储每一层的rank值 */
        rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
        /* 在同一层通过前向指针遍历,直到
        	1.下一个节点为空
        	2.下一节点分数大于等于范围最小值
        	3.节点分数相同元素值更大
        进入下一层 */
        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)))
        {
        	/* 累加span获得rank */
            rank[i] += x->level[i].span;
            x = x->level[i].forward;
        }
        /* 记录每一层最末节点 */
        update[i] = x;
    }
    
	/* 获取一个随机的level层数 */
    level = zslRandomLevel();
    /* 如果新层数大于原跳表最大层数,更新zsl-level,并将超出的层记录下来 */
    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);
    /* 更新新节点和每层新节点前一个节点的forward和span */
    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因为插入了一个节点所以要增加1 */
    for (i = level; i < zsl->level; i++) {
        update[i]->level[i].span++;
    }
	
	/* 更新backward指针 */
    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;
}

4.4.获取成员排位

zslGetRank返回包含给定成员和score的节点的排位
平均O(logN),最坏O(N)

unsigned long zslGetRank(zskiplist *zsl, double score, sds ele) {
    zskiplistNode *x;
    unsigned long rank = 0;
    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))) {
            /* 这一步记录了rank */
            rank += x->level[i].span;
            x = x->level[i].forward;
        }

        /* x might be equal to zsl->header, so test if obj is non-NULL */
        if (x->ele && x->score == score && sdscmp(x->ele,ele) == 0) {
            return rank;
        }
    }
    return 0;
}

4.5. 获取某排位节点

zslGetElementByRank返回跳跃表在给定排位上的节点

zskiplistNode* zslGetElementByRank(zskiplist *zsl, unsigned long rank) {
    zskiplistNode *x;
    /* 记录了遍历过程中的rank累加值 */
    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;
}

参考

  1. 《redis的设计与实现》
  2. redis源码-7.2.1

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

02.Cesium源码编译及搭建开发环境

开始之前&#xff0c;默认你已经掌握了一定的前端知识&#xff0c;文章中用到的一些前端知识不再展开解释&#xff0c;如果你有不明白的地方&#xff0c;请自行学习。 另外&#xff0c;本篇文章及后续的文章首先会使用原生JS的方式 进行实例的开发&#xff0c;Vue版本会在后期文…

办公技巧:Excel日常高频使用技巧

目录 1. 快速求和&#xff1f;用 “Alt ” 2. 快速选定不连续的单元格 3. 改变数字格式 4. 一键展现所有公式 “CTRL ” 5. 双击实现快速应用函数 6. 快速增加或删除一列 7. 快速调整列宽 8. 双击格式刷 9. 在不同的工作表之间快速切换 10. 用F4锁定单元格 1. 快速求…

快速掌握批量合并视频

在日常的工作和生活中&#xff0c;我们经常需要对视频进行编辑和处理&#xff0c;而合并视频、添加文案和音频是其中常见的操作。如何快速而简便地完成这些任务呢&#xff1f;今天我们介绍一款强大的视频编辑软件——“固乔智剪软件”&#xff0c;它可以帮助我们轻松实现批量合…

ACE综述

1、ACE综述 ACE自适配通信环境&#xff08;ADAPTIVE Communication Environment&#xff09;是可自由使用、开放源码的面向对象&#xff08;OO&#xff09;框架&#xff08;framework&#xff09;&#xff0c;它实现了许多用于并发通信软件的核心模式。ACE提供了一组丰…

【juc】countdownlatch实现游戏进度

目录 一、截图示例二、代码示例 一、截图示例 二、代码示例 package com.learning.countdownlatch;import java.util.Arrays; import java.util.Random; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurr…

C++:模板(非类型模板参数、类模板的特化、模板的分离编译)

本文是 C&#xff1a;模板&#xff08;函数模板、类模板&#xff09; 该文的进阶部分&#xff0c;主要介绍模板非类型模板参数、类模板的特化、模板的分离编译这三部分。 目录 一、非类型模板参数 二、模板的特化 1.概念 2.函数模板特化 3.类模板特化 1.全特化 2.偏特…

Lua系列文章(1)---Lua5.4参考手册学习总结

windows系统上安装lua,下载地址&#xff1a; Github 下载地址&#xff1a;https://github.com/rjpcomputing/luaforwindows/releases 可以有一个叫SciTE的IDE环境执行lua程序 1 – 简介 Lua 是一种强大、高效、轻量级、可嵌入的脚本语言。 它支持过程编程&#xff0c; 面向对…

VScode配置Jupyter

环境 安装步骤 1、插件安装 2、更改pip加速源 pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple 参考&#xff1a;vscode python配置pip源 ​​​​​​​ 【Python学习】Day-00 Python安装、VScode安装、pip命令、镜像源配置、虚拟环境 3、建…

202. 最幸运的数字

202. 最幸运的数字 - AcWing题库 #include<bits/stdc.h> #define IOS ios::sync_with_stdio(0);cin.tie(0);cout.tie(0); #define endl \nusing namespace std;typedef pair<int, int> PII; typedef long long ll; typedef long double ld;ll qmi(ll a, ll k, ll m…

集中发现服务DCPSInfoRepo通信端口和ORB交互流程

OpenDDS集中发现服务DCPSInfoRepo,为OpenDDS的pub和sub通信终端提供主题匹配和通信协商和中介服务,是基于TAO的ORB机制完成的,GIOP协议。 1、集中发现服务DCPSInfoRepo的相关通信端口 1)集中发现服务DCPSInfoRepo通信端口 DCPSInfoRepo -ORBListenEndpoints iiop://192.…

RK3568的CAN驱动适配

目录 背景&#xff1a; 1.内核驱动模块配置 2.设备树配置 3.功能测试 4.bug修复 背景&#xff1a; 某个项目上使用RK3568的芯片&#xff0c;需要用到4路CAN接口进行通信&#xff0c;经过方案评审后决定使用RK3568自带的3路CAN外加一路spi转的CAN实现功能&#xff0c;在这个…

SMT求解器Q3B——在WSL上的Docker配置

SMT求解器Q3B——在WSL上的Docker配置 1、配置wsl下的Docker2、在github上下载Q3B3、更换配置文件4、安装docker镜像5、编译Q3B6、使用Q3B 1、配置wsl下的Docker WSL 2 上的 Docker 远程容器入门 2、在github上下载Q3B Q3B下载地址 3、更换配置文件 下载完后&#xff0c;将…

[补题记录] Atcoder Beginner Contest 297(F)

URL&#xff1a;https://atcoder.jp/contests/abc297 目录 F Problem/题意 Thought/思路 Code/代码 F Problem/题意 给一个 H * W 的矩形&#xff0c;在其中任意放置 K 个点&#xff0c;由这 K 个点构成的最小矩形带来的贡献为该矩形的面积&#xff0c;这 K 个点构成一种…

什么是全员生产维护TPM?

在当今竞争激烈的市场环境中&#xff0c;企业不仅需要提高生产效率&#xff0c;同时也需要降低成本以保持竞争力。全员生产维护&#xff08;Total Productive Maintenance&#xff0c;TPM&#xff09;作为一种先进的生产管理方法&#xff0c;为企业提供了一种有效的方式来实现这…

【计算机基础知识】字符的编码表示

欢迎来到我的&#xff1a;世界 希望作者的文章对你有所帮助&#xff0c;有不足的地方还请指正&#xff0c;大家一起学习交流 ! 目录 前言1.西文字符编码2.中文字符编码汉字输入码汉字国标码汉字机内码汉字字形码 总结 前言 计算机处理的数据中&#xff0c;除了数值型数据以外…

【数据结构-哈希表 一】【原地哈希】:缺失的第一个正整数

废话不多说&#xff0c;喊一句号子鼓励自己&#xff1a;程序员永不失业&#xff0c;程序员走向架构&#xff01;本篇Blog的主题是【原地哈希】&#xff0c;使用【数组】这个基本的数据结构来实现&#xff0c;这个高频题的站点是&#xff1a;CodeTop&#xff0c;筛选条件为&…

Day-05 CentOS7.5 安装 Docker

参考 &#xff1a; Install Docker Engine on CentOS | Docker DocsLearn how to install Docker Engine on CentOS. These instructions cover the different installation methods, how to uninstall, and next steps.https://docs.docker.com/engine/install/centos/ Doc…

淘宝商品描述数据API接口

淘宝商品描述数据API接口是淘宝开放平台提供的一种接口&#xff0c;主要用于获取淘宝商品描述信息。该API接口可以帮助开发者在自己的网站或应用程序中快速获取淘宝商品的详细描述信息&#xff0c;包括商品标题、商品描述、商品属性、价格、图片等。 淘宝商品描述数据API接口采…

2023年【广东省安全员C证第四批(专职安全生产管理人员)】报名考试及广东省安全员C证第四批(专职安全生产管理人员)最新解析

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2023年【广东省安全员C证第四批&#xff08;专职安全生产管理人员&#xff09;】报名考试及广东省安全员C证第四批&#xff08;专职安全生产管理人员&#xff09;最新解析&#xff0c;包含广东省安全员C证第四批&…

DWC数字世界大会先导论坛将于10月13日在宁波举办 | 数字技术赋能世界可持续发展

农业经济影响世界数千年&#xff0c;工业经济从欧美发源开始已有数百年&#xff0c;数字经济作为世界未来发展之大势&#xff0c;将成为影响未来数百年的世界命题。在以中国式现代化全面推进中华民族伟大复兴的历史征程中&#xff0c;数字技术、数字经济作为中国式现代化实践最…