Redis基础数据结构之 Sorted Set 有序集合 源码解读

目录标题

  • Sorted Set 是什么?
  • Sorted Set 数据结构
    • 跳表(skiplist)
      • 跳表节点的结构定义
      • 跳表的定义
      • 跳表节点查询
      • 层数设置
  • Sorted Set 基本操作

Sorted Set 是什么?

有序集合(Sorted Set)是 Redis 中一种重要的数据类型,它本身是集合类型,同时也可以支持集合中的元素带有权重,并按权重排序

  • ZRANGEBYSCORE:按照元素权重返回一个范围内的元素
  • ZSCORE:返回某个元素的权重值

在这里插入图片描述

Sorted Set 数据结构

  • 结构定义:server.h
  • 实现:t_zset.c

结构定义是 zset,里面包含哈希表 dict 和跳表 zsl。zset 充分利用了:

  • 哈希表的高效单点查询特性(ZSCORE)
  • 跳表的高效范围查询(ZRANGEBYSCORE)

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

Skiplist:用于快速查找、插入和删除操作,提供近似O(log N)的时间复杂度

Dictionary(Hashtables):用来存储成员与分数的映射关系,确保每个成员的唯一性

跳表(skiplist)

多层的有序链表。下面展示的是 3 层的跳表,头节点是一个 level 数组,作为 level0~level2 的头指针

在这里插入图片描述

跳表节点的结构定义

typedef struct zskiplistNode {// sorted set 中的元素sds ele;// 元素权重double score;// 后向指针(为了便于从跳表的尾节点倒序查找)struct zskiplistNode *backward;// 节点的 level 数组struct zskiplistLevel {// 每层上的前向指针struct zskiplistNode *forward;// 跨度,记录节点在某一层 *forward 指针和该节点,跨越了 level0 上的几个节点unsigned long span;} level[];
} zskiplistNode;

跳表的定义

typedef struct zskiplist {// 头节点和尾节点struct zskiplistNode *header, *tail;unsigned long length;int level;
} zskiplist;

在这里插入图片描述

跳表节点查询

在查询某个节点时,跳表会从头节点的最高层开始,查找下一个节点:
访问下一个节点

  • 当前节点的元素权重 < 要查找的权重
  • 当前节点的元素权重 = 要查找的权重,且节点数据<要查找的数据
    访问当前节点 level 数组的下一层指针
    当前节点的元素权重 > 要查找的权重
//获取跳表的表头
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))) {...x = x->level[i].forward;}...
}

层数设置

几种方法:

  • 每层的节点数约是下一层节点数的一半。
    • 好处:查找时类似于二分查找,查找复杂度可以减低到 O(logN)
    • 坏处:每次插入/删除节点,都要调整后续节点层数,带来额外开销

随机生成每个节点的层数。Redis 跳表采用了这种方法。
Redis 中,跳表节点层数是由 zslRandomLevel 函数决定。

int zslRandomLevel(void) {int level = 1;while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))level += 1;return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}

其中每层增加的概率是 0.25,最大层数是 32。

#define ZSKIPLIST_MAXLEVEL 32 /* Should be enough for 2^64 elements */
#define ZSKIPLIST_P 0.25      /* Skiplist P = 1/4 */
跳表插入节点 zslInsert
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;// 从最高层的 level 开始找for (i = zsl->level-1; i >= 0; i--) {// 每层待插入的位置rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];// forward.score < 待插入 score || (forward.score < 待插入 score && forward.ele < ele)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))) {// 在同一层 level 找下一个节点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);// 设置新建节点的 level 数组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;}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;elsezsl->tail = x;zsl->length++;return x;
}

跳表删除节点 zslDelete

int zslDelete(zskiplist *zsl, double score, sds ele, zskiplistNode **node) {zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;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))){x = x->level[i].forward;}update[i] = x;}x = x->level[0].forward;// 判断节点的 score 和 ele 是否符合条件if (x && score == x->score && sdscmp(x->ele,ele) == 0) {// 删除该节点zslDeleteNode(zsl, x, update);if (!node)// 释放内存zslFreeNode(x);else*node = x;return 1;}return 0; /* not found */
}

Sorted Set 基本操作

首先看下如何创建跳表,代码在 object.c 中,可以看到会调用 dictCreate 函数创建哈希表,之后调用 zslCreate 函数创建跳表。

robj *createZsetObject(void) {zset *zs = zmalloc(sizeof(*zs));robj *o;zs->dict = dictCreate(&zsetDictType,NULL);zs->zsl = zslCreate();o = createObject(OBJ_ZSET,zs);o->encoding = OBJ_ENCODING_SKIPLIST;return o;
}

哈希表和跳表的数据必须保持一致。我们通过 zsetAdd 函数研究一下。

zsetAdd
啥都不说了,都在流程图里。
在这里插入图片描述
首先判断编码是 ziplist,还是 skiplist。

ziplist 编码
里面需要判断是否要转换编码,如果转换编码,则需要调用 zsetConvert 转换成 ziplist 编码,这里就不叙述了。

// ziplist 编码时的处理逻辑
if (zobj->encoding == OBJ_ENCODING_ZIPLIST) {unsigned char *eptr;// zset 存在要插入的元素if ((eptr = zzlFind(zobj->ptr, ele, &curscore)) != NULL) {// 存储要插入的元素时,在 not exist 时更新if (nx) {*out_flags |= ZADD_OUT_NOP;return 1;}……if (newscore) *newscore = score;// 原来的 score 和待插入 score 不同if (score != curscore) {// 先删除原来的元素zobj->ptr = zzlDelete(zobj->ptr, eptr);// 插入新元素zobj->ptr = zzlInsert(zobj->ptr, ele, score);*out_flags |= ZADD_OUT_UPDATED;}return 1;}// zset 中不存在要插入的元素else if (!xx) {// 检测 ele 是否过大 || ziplist 过大if (zzlLength(zobj->ptr) + 1 > server.zset_max_ziplist_entries ||sdslen(ele) > server.zset_max_ziplist_value ||!ziplistSafeToAdd(zobj->ptr, sdslen(ele))) {// 转换成 skiplist 编码zsetConvert(zobj, OBJ_ENCODING_SKIPLIST);} else {// 在 ziplist 中插入 (element,score) pairzobj->ptr = zzlInsert(zobj->ptr, ele, score);if (newscore) *newscore = score;*out_flags |= ZADD_OUT_ADDED;return 1;}} else {*out_flags |= ZADD_OUT_NOP;return 1;}
}
skiplist 编码
// skiplist 编码时的处理逻辑
if (zobj->encoding == OBJ_ENCODING_SKIPLIST) {zset *zs = zobj->ptr;zskiplistNode *znode;dictEntry *de;// 从哈希表中查询新增元素de = dictFind(zs->dict, ele);// 查询到该元素if (de != NULL) {/* NX? Return, same element already exists. */if (nx) {*out_flags |= ZADD_OUT_NOP;return 1;}……if (newscore) *newscore = score;// 权重发生变化if (score != curscore) {// 更新跳表节点znode = zslUpdateScore(zs->zsl, curscore, ele, score);// 让哈希表的元素的值指向跳表节点的权重dictGetVal(de) = &znode->score; /* Update score ptr. */*out_flags |= ZADD_OUT_UPDATED;}return 1;}// 如果新元素不存在else if (!xx) {ele = sdsdup(ele);// 在跳表中插入新元素znode = zslInsert(zs->zsl, score, ele);// 在哈希表中插入新元素serverAssert(dictAdd(zs->dict, ele, &znode->score) == DICT_OK);*out_flags |= ZADD_OUT_ADDED;if (newscore) *newscore = score;return 1;} else {*out_flags |= ZADD_OUT_NOP;return 1;}
}

zsetAdd 整体代码

int zsetAdd(robj *zobj, double score, sds ele, int in_flags, int *out_flags, double *newscore) {/* Turn options into simple to check vars. */int incr = (in_flags & ZADD_IN_INCR) != 0;int nx = (in_flags & ZADD_IN_NX) != 0;int xx = (in_flags & ZADD_IN_XX) != 0;int gt = (in_flags & ZADD_IN_GT) != 0;int lt = (in_flags & ZADD_IN_LT) != 0;*out_flags = 0; /* We'll return our response flags. */double curscore;/* NaN as input is an error regardless of all the other parameters. */// 判断 score 是否合法,不合法直接 returnif (isnan(score)) {*out_flags = ZADD_OUT_NAN;return 0;}/* Update the sorted set according to its encoding. */// ziplist 编码时的处理逻辑if (zobj->encoding == OBJ_ENCODING_ZIPLIST) {unsigned char *eptr;// zset 存在要插入的元素if ((eptr = zzlFind(zobj->ptr, ele, &curscore)) != NULL) {// 存储要插入的元素时,在 not exist 时更新if (nx) {*out_flags |= ZADD_OUT_NOP;return 1;}/* Prepare the score for the increment if needed. */if (incr) {score += curscore;if (isnan(score)) {*out_flags |= ZADD_OUT_NAN;return 0;}}/* GT/LT? Only update if score is greater/less than current. */if ((lt && score >= curscore) || (gt && score <= curscore)) {*out_flags |= ZADD_OUT_NOP;return 1;}if (newscore) *newscore = score;// 原来的 score 和待插入 score 不同if (score != curscore) {// 先删除原来的元素zobj->ptr = zzlDelete(zobj->ptr, eptr);// 插入新元素zobj->ptr = zzlInsert(zobj->ptr, ele, score);*out_flags |= ZADD_OUT_UPDATED;}return 1;}// zset 中不存在要插入的元素else if (!xx) {// 检测 ele 是否过大 || ziplist 过大if (zzlLength(zobj->ptr) + 1 > server.zset_max_ziplist_entries ||sdslen(ele) > server.zset_max_ziplist_value ||!ziplistSafeToAdd(zobj->ptr, sdslen(ele))) {// 转换成 skiplist 编码zsetConvert(zobj, OBJ_ENCODING_SKIPLIST);} else {// 在 ziplist 中插入 (element,score) pairzobj->ptr = zzlInsert(zobj->ptr, ele, score);if (newscore) *newscore = score;*out_flags |= ZADD_OUT_ADDED;return 1;}} else {*out_flags |= ZADD_OUT_NOP;return 1;}}/* Note that the above block handling ziplist would have either returned or* converted the key to skiplist. */// skiplist 编码时的处理逻辑if (zobj->encoding == OBJ_ENCODING_SKIPLIST) {zset *zs = zobj->ptr;zskiplistNode *znode;dictEntry *de;// 从哈希表中查询新增元素de = dictFind(zs->dict, ele);// 查询到该元素if (de != NULL) {/* NX? Return, same element already exists. */if (nx) {*out_flags |= ZADD_OUT_NOP;return 1;}// 从哈希表中查询元素的权重curscore = *(double *) dictGetVal(de);// 如果要更新元素权重值if (incr) {score += curscore;if (isnan(score)) {*out_flags |= ZADD_OUT_NAN;return 0;}}/* GT/LT? Only update if score is greater/less than current. */if ((lt && score >= curscore) || (gt && score <= curscore)) {*out_flags |= ZADD_OUT_NOP;return 1;}if (newscore) *newscore = score;// 权重发生变化if (score != curscore) {// 更新跳表节点znode = zslUpdateScore(zs->zsl, curscore, ele, score);// 让哈希表的元素的值指向跳表节点的权重dictGetVal(de) = &znode->score; /* Update score ptr. */*out_flags |= ZADD_OUT_UPDATED;}return 1;}// 如果新元素不存在else if (!xx) {ele = sdsdup(ele);// 在跳表中插入新元素znode = zslInsert(zs->zsl, score, ele);// 在哈希表中插入新元素serverAssert(dictAdd(zs->dict, ele, &znode->score) == DICT_OK);*out_flags |= ZADD_OUT_ADDED;if (newscore) *newscore = score;return 1;} else {*out_flags |= ZADD_OUT_NOP;return 1;}} else {serverPanic("Unknown sorted set encoding");}return 0; /* Never reached. */
}
zsetDel
int zsetDel(robj *zobj, sds ele) {// ziplist 编码if (zobj->encoding == OBJ_ENCODING_ZIPLIST) {unsigned char *eptr;// 找到对应的节点if ((eptr = zzlFind(zobj->ptr, ele, NULL)) != NULL) {// 从 ziplist 中删除zobj->ptr = zzlDelete(zobj->ptr, eptr);return 1;}}// skiplist 编码else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) {zset *zs = zobj->ptr;// 从 skiplist 中删除if (zsetRemoveFromSkiplist(zs, ele)) {if (htNeedsResize(zs->dict)) dictResize(zs->dict);return 1;}} else {serverPanic("Unknown sorted set encoding");}return 0; /* No such element found. */
}

Redis 的有序集合通过跳跃表和字典的结合,既保证了成员的唯一性,又提供了高效的排序和检索能力,使其成为处理需要排序数据的理想选择。

在这里插入图片描述

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

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

相关文章

SQL Server数据库简单的事务日志备份恢复

模拟数据库备份恢复过程 1.基础操作 1.创建TestDB数据库&#xff0c;并添加数据 USE [master] GO CREATE DATABASE TestDB CONTAINMENT NONE ON PRIMARY ( NAME NTestDB, FILENAME ND:\TestDB.mdf , SIZE 8192KB , MAXSIZE UNLIMITED, FILEGROWTH 65536KB ) LOG ON ( …

【ArcGIS Pro实操第七期】栅格数据合并、裁剪及统计:以全球不透水面积为例

【ArcGIS Pro实操第七期】批量裁剪&#xff1a;以全球不透水面积为例 准备&#xff1a;数据下载ArcGIS Pro批量裁剪数据集1 数据拼接2 数据裁剪3 数据统计&#xff1a;各栅格取值3.1 栅格计算器-精确提取-栅格数据特定值3.2 数据统计 4 不透水面积变化分析 参考 准备&#xff1…

DAY13信息打点-Web 应用源码泄漏开源闭源指纹识别GITSVNDS备份

#知识点 0、Web架构资产-平台指纹识别 1、开源-CMS指纹识别源码获取方式 2、闭源-习惯&配置&特性等获取方式 3、闭源-托管资产平台资源搜索监控 演示案例&#xff1a; ➢后端-开源-指纹识别-源码下载 ➢后端-闭源-配置不当-源码泄漏 ➢后端-方向-资源码云-源码泄漏 …

[C++进阶]AVL树

前面我们说了二叉搜索树在极端条件下时间复杂度为O(n),本篇我们将介绍一种对二叉搜索树进行改进的树——AVL树 一、AVL 树的概念 二叉搜索树虽可以缩短查找的效率&#xff0c;但如果数据有序或接近有序二叉搜索树将退化为单支树&#xff0c;查找效率低下。因此&#xff0c;两位…

人工智能和大模型的简介

文章目录 前言一、大模型简介二、大模型主要功能1、自然语言理解和生成2、文本总结和翻译3、文本分类和信息检索4、多模态处理三、大模型的技术特性1、深度学习架构2、大规模预训练3、自适应能力前言 随着技术的进步,人工智能(Artificial Intelligence, AI)和机器学习(Mac…

Pytorch详解-Pytorch核心模块

Pytorch核心模块 一、Pytorch模块结构_pycache__Cincludelibautogradnnoptimutils 二、Lib\site-packages\torchvisiondatasetsmodelsopstransforms 三、核心数据结构——Tensor&#xff08;张量&#xff09;在深度学习中&#xff0c;时间序列数据为什么是三维张量&#xff1f;…

算法leecode笔记

具体代码&#xff1a; class Solution { public:vector<int> twoSum(vector<int>& nums, int target) {unordered_map<int, int> hashtable;for (int i 0; i < nums.size(); i) {auto it hashtable.find(target - nums[i]);if (it ! hashtable.end…

Git之如何删除Untracked文件(六十八)

简介&#xff1a; CSDN博客专家、《Android系统多媒体进阶实战》一书作者 新书发布&#xff1a;《Android系统多媒体进阶实战》&#x1f680; 优质专栏&#xff1a; Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a; 多媒体系统工程师系列【…

【计算机网络】UDP 协议详解及其网络编程应用

文章目录 一、引言二、UDP1、UDP的协议格式2、UDP 报文的解包和分用3、UDP面向数据报的特点 三、UDP输入输出四、UDP网络编程 一、引言 UDP&#xff08;User Datagram Protocol&#xff0c;用户数据报协议&#xff09;是一种网络通信协议&#xff0c;它属于传输层的协议。是一…

HTTP中的Cookie与Session

一、背景 HTTP协议是无状态无连接的。 无状态&#xff1a;服务器不会保存客户端历史请求记录&#xff0c;每一次请求都是全新的。 无连接&#xff1a;服务器应答后关闭连接&#xff0c;每次请求都是独立的。 无状态就导致服务器不认识每一个请求的客户端是否登陆过。 这时…

【贪心算法】贪心算法

贪心算法简介 1.什么是贪心算法2.贪心算法的特点3.学习贪心的方向 点赞&#x1f44d;&#x1f44d;收藏&#x1f31f;&#x1f31f;关注&#x1f496;&#x1f496; 你的支持是对我最大的鼓励&#xff0c;我们一起努力吧!&#x1f603;&#x1f603; 1.什么是贪心算法 与其说是…

Spring为什么要用三级缓存解决循环依赖?

Spring为什么要用三级缓存解决循环依赖&#xff1f; 1. Spring是如何创建一个bean对象2. Spring三级缓存2.1 一级缓存&#xff1a;单例池&#xff0c;经历过完整bean生命&#xff0c;单例Bean对象2.2 二级缓存&#xff1a;提前暴露的Bean2.3 三级缓存&#xff1a;打破循环 3. S…

计算机网络通关学习(一)

简介 之前我通过王道的考研课进行了计算机网络的学习&#xff0c;但是在秋招准备过程中发现之前的笔记很多不足&#xff0c;学习的知识不够深入和巩固&#xff0c;所以再重新对《图解HTTP》&《图解TCP/IP》进行深度学习后&#xff0c;总结出了此篇博客&#xff0c;由于内容…

08_Python数据类型_字典

Python的基础数据类型 数值类型&#xff1a;整数、浮点数、复数、布尔字符串容器类型&#xff1a;列表、元祖、字典、集合 字典 字典&#xff08;Dictionary&#xff09;是一种可变容器模型&#xff0c;它可以存储任意类型对象&#xff0c;其中每个对象都存储为一个键值对。…

存储数据的树形结构

目录 1、二叉查找树 2、平衡二叉树AVL Tree 3 、平衡多叉树B-Tree 4、BTree树 5 、红黑树 红黑树的应用 6.平衡树的旋转 mysql 索引数据结构&#xff1a; Btree 索引是B树在数据库中的一种实现&#xff0c;最为常见的。B树 中的B代表平衡&#xff0c;而不是二叉 1、二…

带你如何使用CICD持续集成与持续交付

目录 一、CICD是什么 1.1 持续集成&#xff08;Continuous Integration&#xff09; 1.2 持续部署&#xff08;Continuous Deployment&#xff09; 1.3 持续交付&#xff08;Continuous Delivery&#xff09; 二、git工具使用 2.1 git简介 2.2 git的工作流程 2.3 部署g…

如何用 Scrapy 爬取网站数据并在 Easysearch 中进行存储检索分析

做过数据分析和爬虫程序的小伙伴想必对 Scrapy 这个爬虫框架已经很熟悉了。今天给大家介绍下&#xff0c;如何基于 Scrapy 快速编写一个爬虫程序并利用 Easysearch 储存、检索、分析爬取的数据。我们以极限科技的官网 Blog 为数据源&#xff0c;做下实操演示。 安装 scrapy 使…

3. Python计算水仙花数

Python计算水仙花数 一、什么是水仙花数&#xff1f; 百度答案 二、怎样使用Python计算水仙花数&#xff1f; 这里需要for循环&#xff0c;if判断&#xff0c;需要range()函数&#xff0c;需要知道怎么求个位数&#xff0c;十位数&#xff0c;百位数… 1. For循环 语句结…

CTFHub技能树-SQL注入-整数型注入

一、手动注入 思路&#xff1a;注入点->库->表->列->数据 首先使用order by探测有几列 http://challenge-215beae2f0b99b12.sandbox.ctfhub.com:10800/?id1 order by 2 我们发现order by 2 的时候有回显&#xff0c;到了order by 3 的时候就没有回显了&#xf…

k8s的环境配置

一、前期系统环境准备 准备3台主机&#xff1a;硬盘50G cpu2个 内存2G 1、3台主机同时配置 1&#xff09;关闭防火墙与selinux、NetworkManager [rootk8s-master ~]# systemctl stop firewalld[rootk8s-master ~]# systemctl disable firewalldRemoved symlink /etc/systemd/…