聊聊redis中的有序集合

news2025/1/21 0:53:13

写在文章开头

有序集合(sorted set)redis中比较常见的数据库结构,它不仅支持O(logN)界别的有序的范围查询,同时也支持O(1)级别的单元素查询,基于此问题,本文就将从redis源码的角度分析一下有序集合的设计与实现。

在这里插入图片描述

Hi,我是 sharkChili ,是个不断在硬核技术上作死的 java coder ,是 CSDN的博客专家 ,也是开源项目 Java Guide 的维护者之一,熟悉 Java 也会一点 Go ,偶尔也会在 C源码 边缘徘徊。写过很多有意思的技术博客,也还在研究并输出技术的路上,希望我的文章对你有帮助,非常欢迎你关注我的公众号: 写代码的SharkChili

因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。

在这里插入图片描述

详解redis中的有序集合

基本结构介绍

有序集合的实现可以说是集大成于一身,它之所以支持单元素查询和范围查询,是字典(dict)跳表(skipList)的组合结果,它将带有权重的元素指针分别交给字典和跳表进行管理。

关于跳表的介绍,笔者之前也写过一篇关于跳表的设计与实现思路的文章,感兴趣的可以移步JavaGuide官网:

Redis为什么用跳表实现有序集合

当我们需要通过元素名称定位其权重时,我们可以通过哈希算法到字典中定位到对应的元素,以下图为例,若我们希望定位到apple这个元素的权重时,可以直接计算apple的哈希值结合哈希算法即可实现O(1)级别的元素定位。

当我们希望进行范围查询时,很明显哈希表算法的无序性很难快速做到这一点,对应的我们可以直接通过跳表的索引快速定位到指定范围,以下图为例,假设我们希望查询权重在20到30之间的元素,对应的跳表的查询路径为:

  1. apple节点的2级索引。
  2. apple节点的1级索引。
  3. 通过banana的2级索引锁定范围。

这种通过多级索引的方式使得跳表范围查询的时间复杂度为O(logN)级别非常之高效。

在这里插入图片描述

关于有序集合的源码定义,因为该数据结构是组合而非自实现,所以其定义在redis.h这个头文件中:

typedef struct zset {
	//字典
    dict *dict;
    //跳表
    zskiplist *zsl;
} zset;

有序集合的初始化和保存操作

假设我们键入如下指令插入一个权重为1的元素:

ZADD runoobkey 1 dskadjksldksdjsdjskdjksldklsdjlksdjklsadjksladjklsdjksldjksladjskaldjskdjsalkdlksadksdklsadjklsadklsaddkslkadsakdjdlkjsdlkjdlkadjlksadjklsajlksjdjsakldjksadjklsdjlsakdjlsadadasdjsakdljskaldjlksdjlksdjlksajdklsdjksladjklsadjklsajdlksadjlksajdksajdskldjaslkdjsalkdjslkajdskadjlksadjksladjklsadskadjlskajdksajdksaldjksldjkldjsalkdjklsadjlksajdlkadjksajdksladjksladjsdjslakdjlksadjsakldjlksajdlksajdlksajdlksajdlksajdlksajdsladjladskdjlsakjdlksajdlkajdlsakdj

redis服务端收并解出zadd命令后就会调用zaddGenericCommand初始化字典和跳表,再进行元素的保存操作,对应的源码如下,这里笔者省去了有序集合为压缩列表时元素维护的逻辑,可以看到当插入的元素值的长度若大于64字节,则创建的有序集合是由跳表和字典构成。
完成初始化之后,无论是更新还是插入操作,有序集合都会基于该元素的指针将其分别维护到跳表和字典中:

void zaddGenericCommand(redisClient *c, int incr) {
   	//......
   	//判断有序集合是否存在
    zobj = lookupKeyWrite(c->db,key);
    //若为空则当前元素长度大于zset_max_ziplist_value(默认为64)则创建上文所说的有序集合
    if (zobj == NULL) {
        if (server.zset_max_ziplist_entries == 0 ||
            server.zset_max_ziplist_value < sdslen(c->argv[3]->ptr))
        {
        	//创建通过字典和跳表组合实现的有序集合
            zobj = createZsetObject();
        } else {
            zobj = createZsetZiplistObject();
        }
        //将有序集合存入键值对中
        dbAdd(c->db,key,zobj);
    } else {
       //......
    }
	
	//
    for (j = 0; j < elements; j++) {
        score = scores[j];
		//有序集合为压缩列表的逻辑
        if (zobj->encoding == REDIS_ENCODING_ZIPLIST) {
           //......
        } else if (zobj->encoding == REDIS_ENCODING_SKIPLIST) {
            zset *zs = zobj->ptr;
            zskiplistNode *znode;
            dictEntry *de;

            ele = c->argv[3+j*2] = tryObjectEncoding(c->argv[3+j*2]);
            //到哈希表中定位元素并完成保存或者更新操作
            de = dictFind(zs->dict,ele);
            if (de != NULL) {
               //更新操作
            } else {
            	//插入操作,先将其元素插入到跳表
                znode = zslInsert(zs->zsl,score,ele);
               	//......
                //再将元素的指针同时维护到字典中
                redisAssertWithInfo(c,NULL,dictAdd(zs->dict,ele,&znode->score) == DICT_OK);
              	//......
            }
        } else {
            redisPanic("Unknown sorted set encoding");
        }
    }
  //......
}

利用字典完成单值查询

有了这样一个组合的数据结构,不同的查询操作就变得非常方便,例如我们使用Zrank 查询对应有序集合zsetelement-1的权重:

 Zrank  zsets element-1

redis解析该字符串后走到zrankCommand方法,其内部调用zrankGenericCommand,直接通过当前传入的元素的名称到字典中快速定位元素权重并返回:

void zrankCommand(redisClient *c) {
	//调用zrankGenericCommand完成O(1)级别的查询
    zrankGenericCommand(c, 0);
}


void zrankGenericCommand(redisClient *c, int reverse) {
	//获取参数中的key和要查询的元素
    robj *key = c->argv[1];
    robj *ele = c->argv[2];
    robj *zobj;
    unsigned long llen;
    unsigned long rank;

   //......

    redisAssertWithInfo(c,ele,sdsEncodedObject(ele));
	//有序集合为跳表的逻辑,不重要所以直接跳过
    if (zobj->encoding == REDIS_ENCODING_ZIPLIST) {
        //......
    } else if (zobj->encoding == REDIS_ENCODING_SKIPLIST) {
	    //......
	    //通过有序集合的字典快速定位元素并返回
        de = dictFind(zs->dict,ele);
        if (de != NULL) {
            //获取score
            score = *(double*)dictGetVal(de);
            //从跳表中获得对应等级
            rank = zslGetRank(zsl,score,ele);
           //......
           //将结果写出去
            if (reverse)
                addReplyLongLong(c,llen-rank);
            else
                addReplyLongLong(c,rank-1);
        } else {
            addReply(c,shared.nullbulk);
        }
    } else {
        redisPanic("Unknown sorted set encoding");
    }
}

利用跳表完成范围查询

对应的假如我们通过ZRANGE 指令查询索引2到3范围以内的元素:

ZRANGE zsets 2 3 WITHSCORES

该操作对应有序集合中时,会算得我们要获取的是第3、4个元素,此时有序集合就会通过跳表完成这一查询,如下图所示,我们以红色标识为路径即可知晓,因为多级索引的存在,范围查询过程为:

  1. 2 3算的我们要查询的长度为(3-2)+1即2个元素。
  2. 头节点apple的3级索引,其span为2代表跨越两个节点,加上自己本身相当于跨了3个节点定位到了目标元素的起始位置。
  3. 因为我们的查询长度为2,所以orange元素的指针向后1步即可拿到所有元素。
  4. 自此我们拿到元素orangepear,并将score一并写出。

在这里插入图片描述

对应的命令就会走到t_zset.czrangeCommand的逻辑,可以看到其内部本质就是对zrangeGenericCommand的调用,这其中0表示按照升序查询:

void zrangeCommand(redisClient *c) {
	//调用zrangeGenericCommand到跳表获取范围结果
    zrangeGenericCommand(c,0);
}


我们继续步入这段逻辑即可看到跳表整体逻辑,先计算索引的查询范围,然后继续该范围通过跳表定位到第一个符合要求的元素,再基于该元素的指针继续往后查询:

void zrangeGenericCommand(redisClient *c, int reverse) {
    robj *key = c->argv[1];
    robj *zobj;
    int withscores = 0;
    long start;
    long end;
    int llen;
    int rangelen;

   //......

	//根据传入的范围计算本次范围查询的长度rangelen 
    llen = zsetLength(zobj);
    //边界判断
    if (start < 0) start = llen+start;
    if (end < 0) end = llen+end;
    if (start < 0) start = 0;

   //......
    if (end >= llen) end = llen-1;
    //计算查询范围
    rangelen = (end-start)+1;

    //......
    if (zobj->encoding == REDIS_ENCODING_ZIPLIST) {
      //......

    } else if (zobj->encoding == REDIS_ENCODING_SKIPLIST) {//跳表的逻辑
    
        zset *zs = zobj->ptr;
        zskiplist *zsl = zs->zsl;
        zskiplistNode *ln;
        robj *ele;

       
        if (reverse) {//倒叙查找元素
            ln = zsl->tail;
            if (start > 0)
                ln = zslGetElementByRank(zsl,llen-start);
        } else {
        	//升序查询第一个符合要求的元素
            ln = zsl->header->level[0].forward;
            if (start > 0)
                ln = zslGetElementByRank(zsl,start+1);
        }
		//根据范围长度rangelen通过ln的next指针开始不断遍历截取对应个数的元素
        while(rangelen--) {
            redisAssertWithInfo(c,zobj,ln != NULL);
            ele = ln->obj;
            addReplyBulk(c,ele);
            if (withscores)
                addReplyDouble(c,ln->score);
             //基于最底层的叶子节点的forward前进获取后续的元素
            ln = reverse ? ln->backward : ln->level[0].forward;
        }
    } else {
        redisPanic("Unknown sorted set encoding");
    }
}

这里我们重点查看zslGetElementByRank,该函数会传入要查询的第一个元素的编号rank(该值为索引号+1),和我们图解的逻辑基本一致,从跳表首元素的索引开始累加查看span是否等于rank,一旦等于rank就说明当前节点就是我们的要查询范围的第一个元素,有序集合就会将该元素的指针返回:

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--) {
    	//查看当前元素是否还有前驱节点且当亲累加结果是否小于rank,若都符合要求则说明当前还没到到达要查询的第一个元素的位置,进入该循环,不断向前或者向下跨span
        while (x->level[i].forward && (traversed + x->level[i].span) <= rank)
        {
            traversed += x->level[i].span;
            x = x->level[i].forward;
        }
        //经过的节点长度traversed 等于第一个元素的编号rank时将该指针返回
        if (traversed == rank) {
            return x;
        }
    }
    return NULL;
}

小结

自此我们就将redis中有序集合的最核心的思想和设计都分析完成了,希望对你有帮助。

我是 sharkchiliCSDN Java 领域博客专家开源项目—JavaGuide contributor,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili
因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。

在这里插入图片描述

参考

《redis设计与实现》

Redis 有序集合(sorted set):https://www.runoob.com/redis/redis-sorted-sets.html

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

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

相关文章

黄仁勋最新对话:未来互联网流量将大幅减少,计算将更多即时生成

黄仁勋表示&#xff0c;未来随着设备上运行的小语言模型变得更加上下文化和生成化&#xff0c;互联网流量将大幅减少&#xff0c;计算将更多地即时生成&#xff0c;这将极大地节省能源&#xff0c;使计算模型发生根本性转变。 要点 1、黄仁勋强调生成式AI正以指数速度增长&…

C++ 65 之 模版的局限性

#include <iostream> #include <cstring> using namespace std;class Students05{ public:string m_name;int m_age;Students05(string name, int age){this->m_name name;this->m_name age;} };// 两个值进行对比的函数 template<typename T> bool …

APS计划排程系统如何打破装备使用约束

APS计划排程系统是离散制造型企业在计划控制方向的重要支撑&#xff0c;它提供的是交期预测、订单排产计划、物料采购计划、人力分配计划等等。近些几年来&#xff0c;多品种、小批量、多订单的生产模式&#xff0c;让企业的计划员应接不暇、疲累不堪&#xff0c;传统的人工经验…

js语法---理解防抖原理和实现方法

什么是防抖&#xff08;节流&#xff09; 在实际的网页交互中&#xff0c;如果一个事件高频率的触发&#xff0c;这会占用很多内存资源&#xff0c;但是实际上又并不需要监听触发如此多次这个事件&#xff08;比如说&#xff0c;在抢有限数量的优惠券时&#xff0c;用户往往会提…

白鲸开源CEO郭炜受邀参加第二届软件创新大会,探讨开源与AI融合实践下的商业模式创新

6月13日至6月14日&#xff0c;第二届软件创新发展大会在武汉光谷正式拉开帷幕。此次大会由武汉市人民政府、湖北省经济和信息化厅主办&#xff0c;国家工业信息安全发展研究中心、武汉市经济和信息化局、武汉东湖新技术开发区管理委员会共同承办&#xff0c;旨在贯彻落实国家软…

零基础入门学用Arduino 第四部分(二)

重要的内容写在前面&#xff1a; 该系列是以up主太极创客的零基础入门学用Arduino教程为基础制作的学习笔记。个人把这个教程学完之后&#xff0c;整体感觉是很好的&#xff0c;如果有条件的可以先学习一些相关课程&#xff0c;学起来会更加轻松&#xff0c;相关课程有数字电路…

笔记-python里面的xlrd模块详解

那我就一下面积个问题对xlrd模块进行学习一下&#xff1a; 1.什么是xlrd模块&#xff1f; 2.为什么使用xlrd模块&#xff1f; 3.怎样使用xlrd模块&#xff1f; 1.什么是xlrd模块&#xff1f; ♦python操作excel主要用到xlrd和xlwt这两个库&#xff0c;即xlrd是读excel&…

leetcode118 杨辉三角

给定一个非负整数 numRows&#xff0c;生成「杨辉三角」的前 numRows 行。 在「杨辉三角」中&#xff0c;每个数是它左上方和右上方的数的和。 示例 1: 输入: numRows 5 输出: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]示例 2: 输入: numRows 1 输出: [[1]] public List…

网络安全:探索云安全的最佳实践

文章目录 网络安全&#xff1a;探索云安全的最佳实践引言云安全简介云安全面临的挑战云安全的最佳实践数据加密身份和访问管理定期安全审计 结语 网络安全&#xff1a;探索云安全的最佳实践 引言 在我们之前的文章中&#xff0c;我们讨论了网络安全的多个方面&#xff0c;包括…

【Python特征工程系列】基于方差分析的特征重要性分析(案例+源码)

这是我的第304篇原创文章。 一、引言 方差分析&#xff08;Analysis of Variance&#xff0c;简称ANOVA&#xff09;是一种统计方法&#xff0c;用于比较两个或多个组之间的平均值是否存在显著差异。 方法简介&#xff1a; ANOVA 通过分解总方差为组间方差和组内方差&#x…

鼠情自动监测系统

TH-SH1在农业生产中&#xff0c;鼠害问题一直是困扰农民的一大难题。传统的鼠害防治方法往往依赖于大规模施药或布置捕鼠器等方式&#xff0c;这些方法不仅效率低下&#xff0c;而且容易对环境造成污染。随着科技的不断发展&#xff0c;鼠情自动监测系统应运而生&#xff0c;为…

韶关化工安全生产新篇章:可燃气体报警器的校准与检测实践

在韶关这座工业城市&#xff0c;化工行业是当地经济发展的重要支柱。然而&#xff0c;随着化工生产的不断发展&#xff0c;可燃气体泄漏的风险也日益增加。因此&#xff0c;可燃气体报警器在保障生产安全方面扮演着至关重要的角色。 在这篇文章中&#xff0c;佰德将深入探讨可…

XTDrone-无人机与无人船协同初步-配置教程

说明&#xff1a;配置该教程时所使用的是Ubuntu20.04 1 海洋与无人船仿真环境搭建 cp -r ~/XTDrone/sitl_config/usv/* ~/catkin_ws/src/ cd catkin_ws catkin build # or catkin_make 说明&#xff1a;由于官方所编写的脚本时几年之前的&#xff0c;所以很多东西不符合现在…

GD32错误调试篇:串口通讯乱码/stm32移植到GD32后串口通讯乱码等问题

本文章基于兆易创新GD32 MCU所提供的2.2.4版本库函数开发 向上代码兼容GD32F450ZGT6中使用 后续项目主要在下面该专栏中发布&#xff1a; https://blog.csdn.net/qq_62316532/category_12608431.html?spm1001.2014.3001.5482 感兴趣的点个关注收藏一下吧! 电机驱动开发可以跳转…

OpenFeign服务调用与负载均衡

目录 介绍使用高级特性超时控制重试机制默认HttpClient修改请求/响应报文压缩日志打印功能 相关文献 介绍 官网说明&#xff1a; Feign 是一个声明式 Web 服务客户端。它使编写 Web 服务客户端变得更加容易。要使用 Feign&#xff0c;请创建一个接口并对其进行注释。它具有可…

《OKR工作法》读书笔记

花了两个晚上的时间看完了《OKR工作法》这本书&#xff0c;谈不上有什么感想&#xff0c;因为工作后&#xff0c;其实就一直在用这种方法&#xff0c;所谓当局者迷嘛&#xff0c;习以为常也就谈不上多少新的启发。所以&#xff0c;这篇文章纯粹是一篇读书笔记&#xff0c;把我认…

Android网络性能监控方案 android线上性能监测

1 Handler消息机制 这里我不会完整的从Handler源码来分析Android的消息体系&#xff0c;而是从Handler自身的特性引申出线上卡顿监控的策略方案。 1.1 方案确认 首先当我们启动一个App的时候&#xff0c;是由AMS通知zygote进程fork出主进程&#xff0c;其中主进程的入口就是Ac…

PEI是聚醚酰亚胺(Polyetherimide)在粘接使用时使用UV胶水的优势有哪些?要注意哪些事项?

PEI是聚醚酰亚胺&#xff08;Polyetherimide&#xff09;在粘接使用时使用UV胶水的优势有哪些&#xff1f;要注意哪些事项&#xff1f; 在使用UV胶水进行聚醚酰亚胺&#xff08;Polyetherimide&#xff0c;PEI&#xff09;粘接时&#xff0c;有一些优势和注意事项&#xff1a; …

数据库物理计划执行指南

一、背景介绍 伴随信息技术地迅猛发展和应用范围地逐步扩大&#xff0c;数据库已成为企业存储与管理数据的重要工具。但数据量激增以及用户访问需求的与日剧增&#xff0c;数据库性能也将面临巨大挑战。 好在数据库物理计划执行是解决数据库性能问题的重要手段之一&#xff0…

【2024最新精简版】Kafka面试篇

文章目录 Kafka和RabbitMQ什么区别讲一讲Kafka架构你们项目中哪里用到了Kafka?为什么会选择使用Kafka? 有什么好处 ?使用Kafka如何保证消息不丢失 ?消息的重复消费问题如何解决的 ?Kafka如何保证消费的顺序性 ?Kafka的高可用机制有了解过嘛 ?Kafka实现高性能的设计有了解…