【Redis】基础数据结构-skiplist跳跃表

news2024/11/14 12:22:12

有序集合Sorted Set

zadd

zadd用于向集合中添加元素并且可以设置分值,比如添加三门编程语言,分值分别为1、2、3:

127.0.0.1:6379> zadd language 1 java
(integer) 1
127.0.0.1:6379> zadd language 2 c++
(integer) 1
127.0.0.1:6379> zadd language 3 python
(integer) 1

zrange

zrange根据分值区间返回符合条件的数据:

127.0.0.1:6379> zrange language 1 3 withscores
1) "c++"
2) "2"
3) "python"
4) "3"

zscore

zscore根据key和元素值返回元素的分值

127.0.0.1:6379> zscore language python
"3"

Sorted Set是Redis中的一种数据结构,它可以用来存储带有分值的元素,并且根据分值进行排序,是一个有序的集合。

Sorted Set的结构定义如下,它包含了一个哈希表dict和一个跳跃表zskiplist,其中哈希表可以在O(1)的时间复杂度内进行元素查找,而跳跃表可以支持高效的范围查询:

typedef struct zset {
    dict *dict;
    zskiplist *zsl;
} zset;

跳跃表

如果一个有序集合中包含的元素数量比较多或者有序集合中的元素是比较长的字符串时,Redis就会使用跳跃表作为有序集合的底层实现。

跳跃表是一种多层的有序链表,在一个普通的有序链表中如果想要查找某个元素,必须遍历链表,时间复杂度为O(n),那么如何提高查找效率呢,可以使用跳跃表,从列表中抽出一些元素进行分层,比如每隔一个节点就抽出一层:

此时如果需要查找元素为9的节点:

  1. 从第三层开始查找,元素的值为8,因为9大于8并且8之后没有其他的节点所以接下来进入第二层
  2. 进入第二层,8的下一个节点为15,9小于15,所以进入第一层
  3. 进入第一层,获取8的下一个节点,等于要查找的值9,查找结束

结构定义

跳跃表的结构定义

  • header:指向跳跃表中节点的头指针,跳跃表中的节点定义为zskiplistNode,跳跃表实际上也是一个链表,所以会有一个头结点
  • tail:指向跳跃表中节点的尾指针
  • length:跳跃表中节点的数量
  • level:跳跃表的层级
// 跳跃表
typedef struct zskiplist {
    // 指向跳跃表的头尾指针
    struct zskiplistNode *header, *tail;
    // 长度
    unsigned long length;
    // 层级
    int level;
} zskiplist;

节点的结构定义

  • ele:一个sds类型的变量,存储实际的数据
  • score:存储数据的分值,跳跃表就是按照这个分值进行排序的
  • backward:一个指向前一个节点的指针,为了便于从后往前查找
  • zskiplistLevel:一个层级数组,因为跳跃表可以有多层,每一层中都有一个指向当前层级中的下一个节点的指针forward和span跨度,跨度代表了当前层级里面,当前节点与下一个节点直接跨越了几个节点
//跳跃表中的节点结构定义
typedef struct zskiplistNode {
    // 存储的元素
    sds ele;
    // 分值
    double score;
    // 后向指针,指向当前节点的前一个节点
    struct zskiplistNode *backward;
    // 层级数组
    struct zskiplistLevel {
        // 指向当前层级中的下一个节点
        struct zskiplistNode *forward;
        // 跨度
        unsigned long span;
    } 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;
}

/* 创建跳跃表 */
zskiplist *zslCreate(void) {
    int j;
    zskiplist *zsl;
    // 跳跃表分配内存
    zsl = zmalloc(sizeof(*zsl));
    // 层级初始化为1
    zsl->level = 1;
    // 长度为0
    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;
    }
    // 头结点的下一个节点指向NULL
    zsl->header->backward = NULL;
    // 尾节点
    zsl->tail = NULL;
    return zsl;
}

跳跃表层数的设置

跳跃表根据什么规则来进行层数划分呢,有以下几种方案:

方案一

每隔一个节点,就取出一个节点作为新的一层的节点,这样每一层上节点的数量大约是下一层节点数的一半,此时类似于二分查找,查找复杂度为O(logn)

优点:查找的时间复杂度降低了

缺点:由于需要维护每个层级的节点数,在节点进行插入或者删除的时候,要调整层级节点,带来额外的开销

方案二

新增加节点的时候,调用随机生成层数方法,随机生成一个当前跳跃表所需要的层数,如果生成的层数等于当前层数,新节点只需要加入跳跃表中即可,不需要额外的维护每一个层级的节点数,Redis中就是使用的随机生成层数的方式维护跳跃表的层级。

随机生成层数方法:

#define ZSKIPLIST_MAXLEVEL 32  // 最大层级不超过32
#define ZSKIPLIST_P 0.25 

// 随机生成层数
int zslRandomLevel(void) {
    int level = 1;
    // 如果生成的随机数的值小于ZSKIPLIST_P,层数就+1
    while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
        level += 1;
    // 是否超过了最大层数,超过就使用最大层数
    return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}

0xFFFF = 65535

random()&0xFFFF运算之后会生成一个0和65535之间的数,ZSKIPLIST_P * 0xFFFF = 0.25 * 65535,所以random()&0xFFFF 小于 0.25 * 65535的概率为25%,也就是层数会增加1的概率不超过25%。

跳跃表增加节点

  1. 因为跳跃表有多层,所以需要遍历每一层,寻找每层要插入的位置,update[i]就记录了每一层要插入的位置
  2. 随机生成跳跃表的层数,如果层数有变化,则需要调整跳跃表的层高
  3. 创建节点,并将节点插入到跳跃表中
  4. 设置backward,新插入节点的前一个节点是update[0],如果update[0]为头结点,当前节点的前一个节点设为null,否则backward设置为update[0]
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;
    /* 寻找每层要插入的位置,从高层开始向下遍历 */
    for (i = zsl->level-1; i >= 0; i--) {
        // rank[i]记录了当前层从header节点到update[i]节点所经历的步长
        rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
        // 如果当前层级下一个节点不为空 并且 下一个节点的score小于要插入节点的分值 或者 下一个节点的score等于要插入节点的score并且对比两个节点存储的元素值之后小于0(字符串比较)
        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]的值
            rank[i] += x->level[i].span;
            // 获取下一个节点
            x = x->level[i].forward;
        }
        // 记录每层需要插入的位置
        update[i] = x;
    }
    // 随机生成跳跃表的层数
    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;
        x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
        update[i]->level[i].span = (rank[0] - rank[i]) + 1;
    }

    /* 更新跨度 */
    for (i = level; i < zsl->level; i++) {
        update[i]->level[i].span++;
    }
    // 设置当前节点的前一个节点,如果update[0]为头结点,当前节点的前一个节点设为null,否则backward设置为update[0]
    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. Sorted Set支持在添加元素的时候为元素添加一个分值,并根据分值排序,是一个有序的集合。
  2. Sorted Set在数据比较少的时候采用ziplist存储,超过阈值后使用哈希表和跳跃表来提高查找效率,其中哈希表用于单值查询,跳跃表用于范围查询。
  3. 跳跃表是一个多层的有序链表,它采用了空间换时间的方式将查找的时间复杂度降到了O(logN)。

参考

黄健宏《Redis设计与实现》

陈雷《Redis5设计与源码分析》

极客时间 - Redis源码剖析与实战(蒋德钧)

【unix21】redis源码分析–zslRandomLevel位运算解析

【有梦想的肥宅】Redis5设计与源码分析读后感(三)跳跃表

Redis版本:redis-6.2.5

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

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

相关文章

Swift 5.9 与 SwiftUI 5.0 中新 Observation 框架应用之深入浅出

0. 概览 Swift 5.9 一声炮响为我们带来全新的宏&#xff08;Macro&#xff09;机制&#xff0c;也同时带来了干霄凌云的 Observation 框架。 Observation 框架可以增强通用场景下的使用&#xff0c;也可以搭配 SwiftUI 5.0 而获得双剑合璧的更强威力。 在本篇博文&#xff0c…

计算机网络笔记3 数据链路层

计算机网络系列笔记目录&#x1f447; 计算机网络笔记6 应用层计算机网络笔记5 运输层计算机网络笔记4 网络层计算机网络笔记3 数据链路层计算机网络笔记2 物理层计算机网络笔记1 概述 文章前言 &#x1f497; 站在巨人的肩膀上&#xff0c;让知识的获得更加容易&#xff01…

给列起别名(关键字:as)

MySQL从小白到总裁完整教程目录:https://blog.csdn.net/weixin_67859959/article/details/129334507?spm1001.2014.3001.5502 语法格式: select 列名1 as 别名1, 列名2 as 别名2, 列名n as 别名n from 表名; 说明&#xff1a;可以省略as&#xff0c;列名和别名之间使用空格…

力扣 -- 873. 最长的斐波那契子序列的长度

解题步骤&#xff1a; 参考代码&#xff1a; class Solution { public:int lenLongestFibSubseq(vector<int>& nums) {int nnums.size();unordered_map<int,int> hash;for(int i0;i<n;i){hash[nums[i]]i;}int ret2;vector<vector<int>> dp(n,v…

关于内存对齐你需要了解的事

简介 这篇文章的目的是带你了解什么是内存对齐&#xff0c;具体的内存对齐的细节、处理方式、不同架构则不会去详细讲解&#xff0c;只做科普文用。 1、什么是内存对齐 内存对齐和数据在内存中的位置有关。内存对齐以字节为单位进行&#xff0c;一个变量的内存地址如果正好等于…

集合(容器)-List接口及实现类

容器的特征&#xff1a;①数据长度可变&#xff1b;②数据保存方式不同。 集合体系概述&#xff1a;JAVA的集合框架是由很多接口、抽象类、具体类组成。都位于java.util包中。 Java中集合类中默认可以存储任意数据类型&#xff0c;Java中的集合提供泛型机制&#xff0c;在定义…

mysql5.7停止维护时间

mysql5.7将于2023年10月停止官网支持和更新&#xff1b;老项目要准备升级&#xff0c;新项目的mysql必须是mysql8.0&#xff08;2023-10&#xff09; 官方升级咨询地址 oracle官方升级咨询地址https://go.oracle.com/LP116153?elq_mid247718&sh1518132002061316121320310…

.Net开源迁移框架FluentMigrator的使用。

在实际的开发过程中&#xff0c;经常会遇到数据库结构变动&#xff0c;比如新增表、删除表&#xff1b;已有的表新增字段&#xff0c;删除字段&#xff1b;修改字段属性等等。而且需要开发环境、测试环境和生产环境进行同步。如果使用的是EF&#xff0c;还是挺方便的。而非EF环…

Android笔记:Android 组件化方案探索与思考

组件化项目&#xff0c;通过gradle脚本&#xff0c;实现module在编译期隔离&#xff0c;运行期按需加载&#xff0c;实现组件间解耦&#xff0c;高效单独调试。 先来一张效果图 组件化初衷 APP版本不断的迭代&#xff0c;新功能的不断增加&#xff0c;业务也会变的越来越复杂…

【多模态融合】TransFusion学习笔记(2)

接上篇【多模态融合】TransFusion学习笔记(1)。 从TransFusion-L到TransFusion ok,终于可以给出论文中那个完整的框架图了&#xff0c;我第一眼看到这个图有几个疑问: Q&#xff1a;Image Guidance这条虚线引出的Query Initialization是什么意思? Q&#xff1a;图像分支中的…

flex布局与几个实例(含源码)

本文简单的说明下flex布局 有源码实例&#xff0c;后续会持续添加 flex默认主轴是横轴 容器主要有6个属性 flex-direction 决定主轴的方向 flex-direction: row | row-reverse | column | column-reverse; flex-wrap 决定是否换行 flex-wrap: nowrap | wrap | wrap-revers…

JavaAPI---replace

package daysreplace;public class ReplaceTest {public static void main(String[] args) {String str "wwxhhhhhhhhhhh333";System.out.println("替换前的字符串" str);String newstr str.replace("333", "111");System.out.prin…

(第2遍中)内存的堆空间不够 error: MSB3073 超过了 PCH 的虚拟内存范围

压缩包里打开的新工程文件&#xff0c;运行 GenerateProjectFiles.bat 后&#xff0c;再点击 .sln 文件&#xff0c;编译工程&#xff0c;编译了 1.5h 左右&#xff0c;快结束的时候报错如下&#xff1a; 编译器的堆空间不足在第2遍中编译器的堆空间不足error MSB3073: 命令“…

【typescript】面向对象(下篇),包含接口,属性的封装,泛型

假期第八篇&#xff0c;对于基础的知识点&#xff0c;我感觉自己还是很薄弱的。 趁着假期&#xff0c;再去复习一遍 面向对象&#xff1a;程序中所有的操作都需要通过对象来完成 计算机程序的本质就是对现实事物的抽象&#xff0c;抽象的反义词是具体。比如照片是对一个具体的…

大恒IFrameData IImageData转bmp HObject Mat

大恒工业相机采集的帧数据转为其他8bit图像格式 C#转为bmp格式转为Halcon的HObject格式转为OpenCVSharp的Mat格式 回调采集图像的数据类型为IFrameData&#xff0c;单帧采集的数据类型为IImageData&#xff0c;两者的区别为IImageData类多了一个**Destroy()**方法 C# 转为bm…

floyd算法细节

这个不是一篇学习性文章 主要是针对这几天思考的问题进行一些回答 floyD在计网和数据结构和图模型中有广泛的应用算法 很简单但是其中蕴含的原理值得细究。 弗洛伊德算法(Floyd)主要针对多源最短路径,且可以解决路径中有负权的情况(不包含负权回路),但是迪杰斯特拉算法只…

mycat实现mysql读写分离

架构图&#xff1a; 视频地址

逐步解决Could not find artifact com:ojdbc8:jar:12

Could not find artifact com:ojdbc8:jar:12 in central (https://repo.maven.apache.org/maven2) 原因&#xff1a; ojdbc8:jar:12 属于Oracle 数据库链接的一个程序集&#xff0c;缺失的话很有可能会影响数据库链接&#xff0c;蝴蝶效应产生不可预测的BUG&#xff01;但是版…

typescript开发环境搭建

typescript是基于javascript的强类型标记性语言&#xff0c;使用typescript语言可开发出不同规模的、易于扩展的web前端页面应用&#xff0c;本文主要描述typescript的开发环境搭建。 npm install -g typescript 如上所示&#xff0c;在本地开发环境中&#xff0c;使用nodejs…

arduino嵌入式1,LED闪烁案例

CVE系列在等等吧&#xff0c;环境我有点懒得搭建了 文章目录 前言一、anduino是什么玩意儿&#xff1f;二、使用步骤1.找蓝图/画蓝图2.写入数据成果 总结 前言 最近在学习嵌入式开发&#xff0c;我的单片机到了&#xff0c;然后我就沉迷于嵌入式开发的环境中 提示&#xff1a;…