[Redis]1-高效的数据结构P2-Set

按照惯例,先丢一个官网文档链接。

上篇我们已经了解了高效的数据结构P1-String与Hash。
这篇,我们继续来了解Redis的 Set 与 Sorted set。

目录

  • 有序集合 Sorted set
    • 底层实现
  • 集合 Set
  • 总结
  • 资料引用

有序集合 Sorted set

Redis 有序集合是一组唯一的字符串(成员)集合,这些字符串根据一个关联的分数进行排序。

这种有序、元素唯一且根据关联的分数进行排序的高效操作的数据结构,简称ZSET。
可以用于:

  • 动态排序:比如排行榜,每个元素可以代表一个实体(如用户、商品),分数表示排序依据(如积分、销量)。由于ZSET自动维护排序,你可以轻松获取排名靠前的成员、某个成员的排名,或者按分数范围查询。
# 添加
> zadd prices 8 sandwich 
(integer) 1> zadd prices 100000 car 
(integer) 1> zadd prices 6300 iphone 8900 iphonepro
(integer) 2# 结果展示
> zrange prices 0 9 withscores
1) "sandwich"
2) "8"
3) "iphone"
4) "6300"
5) "iphonepro"
6) "8900"
7) "car"
8) "100000"
  • 速率限制器:ZSET可以实现一种基于滑动窗口的速率限制器,利用时间戳作为分数,成员记录请求标识,自动移除过期的请求。

底层实现

ZSET通过包含跳表和哈希表的二端口数据结构实现。每个ZSET对象包含一个哈希表和一个跳表,成员和分数在两边各存一份,哈希表存member -> score,跳表存score -> member的排序关系。

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

其中哈希结构上章已经了解过了。ZSET的唯一性便是通过Hash Table实现的。总体结构也同Hash类似。

跳表用于维护成员的按分数排序,支持高效的插入、删除、排名查询和范围查询。
本篇进行跳表skiplist的介绍,并了解skiplist是如何设计以支持排序的。

源码地址点这,其结构由redis.h/zskiplistNode和redis.h/zskiplist两个结构定义。

zskiplist和zskiplistNode结构如下:

typedef struct zskiplist {struct zskiplistNode *header, *tail; //头、尾节点unsigned long length; // 长度int level;//记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计算在内)
} zskiplist;typedef struct zskiplistNode {sds ele;              // 成员double score;         // 分数struct zskiplistNode *backward;// 后退指针struct zskiplistLevel {struct zskiplistNode *forward;// 前进指针unsigned long span;// 跨度,记录跳过的节点数(前进指针所指向节点和当前节点的距离)} level[];
};

zskiplistNode用于表示跳跃表节点,zskiplist结构用于保存跳跃表节点的相关信息。
zskiplistNode点的level数组可以包含多个元素,每个元素都包含一个指向其他节点的指针,程序可以通过这些层来加快访问其他节点的速度,一般来说,层的数量越多,访问其他节点的速度就越快。

较传统Node与List,zskiplistNode多了一个level[]结构,这是一个动态数组。每个元素表示该节点在某一层级的前进指针(指向下一个节点)和跨度(span,表示跳过的节点数)

    struct zskiplistLevel {struct zskiplistNode *forward;// 前进指针unsigned long span;// 跨度,记录跳过的节点数(前进指针所指向节点和当前节点的距离)} level[];

Redis zskiplistNode这个设计通过level[]存储的多层索引预计算节点关系(预存关系),让查找、插入和删除的复杂度从传统链表的O(N)降到O(log N),接近二分查找的效率。
跳表的高层索引节点稀疏,低层节点密集,类似二分搜索的层次划分,快速定位目标节点或分数范围。

level[]有点类似闭包表的核心表设计,存储节点关系。

![[Pasted image 20250417234839.png]]

  • 核心方法
    阅读Redis的zskiplist.c,重点看zslInsert(插入)和zslGetRank(排名计算),理解level[]和span的实现。
    zslInsert源码如下:
/* 插入一个新节点到跳表中,返回新插入的节点指针* 参数:*   zsl: 跳表对象*   score: 新节点的分数*   ele: 新节点的成员(字符串)* 返回:*   新插入的节点指针*/
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x; // update数组记录每层插入位置的前驱节点unsigned long rank[ZSKIPLIST_MAXLEVEL];       // rank数组记录每层累积的跨度int i, level;serverAssert(!isnan(score)); // 确保分数不是NaN// 步骤1:查找插入位置,记录前驱节点和跨度x = zsl->header; // 从头节点开始for (i = zsl->level-1; i >= 0; i--) { // 从最高层逐层向下查找// 初始化rank,继承上一层的跨度(若非最高层)rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];// 沿当前层前进,直到遇到分数更大或字典序更大的节点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;}// 步骤2:随机生成新节点的层级level = zslRandomLevel(); // 随机层级,概率递减(通常p=0.25)if (level > zsl->level) { // 如果新层级超过当前最大层级for (i = zsl->level; i < level; i++) {rank[i] = 0; // 新层级的rank初始化为0update[i] = zsl->header; // 新层级的前驱是头节点update[i]->level[i].span = zsl->length; // 跨度设为跳表总长度}zsl->level = level; // 更新跳表最大层级}// 步骤3:创建新节点x = zslCreateNode(level, score, ele); // 分配新节点内存,初始化分数和成员for (i = 0; i < level; i++) { // 为每层设置指针和跨度// 插入新节点:将新节点的forward指向前驱的forwardx->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; // 前驱的新跨度}// 步骤4:处理更高层的跨度for (i = level; i < zsl->level; i++) {update[i]->level[i].span++; // 未插入新节点的层,跨度+1}// 步骤5:设置后退指针x->backward = (update[0] == zsl->header) ? NULL : update[0];if (x->level[0].forward)x->level[0].forward->backward = x;elsezsl->tail = x;// 步骤6:更新跳表元数据zsl->length++; // 跳表长度+1return x; // 返回新节点
}

集合 Set

Redis 集合是一个无序且唯一的字符串集合(成员)。您可以使用 Redis 集合高效地:

  • 跟踪唯一的项目(例如,跟踪访问特定博客文章的所有唯一 IP 地址。唯一事件ID)
  • 表示关系(例如,具有给定角色的所有用户的集合)
  • 执行常见的集合操作,如交集、并集和差集

和Java的HashSet一样,非常适合删除重复数据的集合。

Redis Set的底层实现主要依赖两种数据结构:哈希表(Hash Table)和整数集合(Intset)

其中哈希结构上章已经了解过了。唯一性便是通过Hash Table实现的。那么整数集合Intset是干什么的呢?
用来提供动态数据结构选择的。

当Set包含非整数成员(如字符串)或成员数量较多时,使用哈希表。
当Set的所有成员都是整数(支持int16、int32、int64),且成员数量较少时(受set-max-intset-entries配置控制,默认512),使用整数集合Intset。
数量超过阈值会转换成Hash Table,且Intset到哈希表的转换是单向的(不可逆),因为哈希表支持任意字符串,而Intset只支持整数。

即,Redis Set的底层数据结构会根据存储的成员类型和数量动态选择。
intset源码地址。
源码如下:

typedef struct intset {// 编码类型(INTSET_ENC_INT16/32/64)uint32_t encoding;// 数组长度uint32_t length;// 保存元素的数组int8_t contents[];
} intset;

Intset是一个紧凑的有序数组,存储整数值,自动选择最小编码类型(int16_t、int32_t、int64_t)以节省内存。
Intset有编码升级机制:
当插入的整数超出当前编码范围(如int16_t溢出),Intset自动升级到更高编码(如int32_t),并重新分配内存。
且Intset有序。Intset按数值大小排序,插入时使用二分查找定位。

Redis通过设计一种转换机制,使用Intset来专门优化存储小规模整数集合,达到节省内存(紧凑存储)的目的,提升内存效率,且支持快速二分查找,适合小集合的查询。

总结

Redis不负简单高效的内存数据库之名,一方面做了大量空间换时间的操作,一方面设计极致压榨内存、提升内存效率。
比如跳表的预存、hash表的渐进扩容、String sds的预留空间、延迟释放、intset的极致内存利用、set的动态转换。

资料引用

《Redis设计与实现》

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

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

相关文章

Python + Playwright:使用正则表达式增强自动化测试

Python + Playwright:使用正则表达式增强自动化测试 前言一、 为什么选择正则表达式?二、 Playwright 中集成正则表达式:途径与方法三、 实战应用:正则表达式解决典型测试难题场景 1:定位 ID 或 Class 包含动态部分的元素场景 2:验证包含可变数字或文本的提示信息场景 3:…

VASP 6.4.1 Ubuntu系统编译安装手册

VASP 6.4.1 Ubuntu系统编译安装手册 &#xff08;基于Ubuntu 22.04 LTS&#xff0c;适用x86_64架构&#xff09; 文章目录 VASP 6.4.1 Ubuntu系统编译安装手册第一章 系统环境深度配置1.1 硬件兼容性验证1.2 操作系统环境准备1.3 数学库深度优化配置 第二章 编译环境深度调优2…

uniapp h5接入地图选点组件

uniapp h5接入地图选点组件 1、申请腾讯地图key2、代码接入2.1入口页面 &#xff08;pages/map/map&#xff09;templatescript 2.2选点页面&#xff08;pages/map/mapselect/mapselect&#xff09;templatescript 该内容只针对uniapp 打包h5接入地图选点组件做详细说明&#x…

java输出、输入语句

先创建一个用于测试的java 编写程序 #java.util使java标准库的一个包&#xff0c;这里拉取Scanner类 import java.util.Scanner;public class VariableTest {public static void main(String[] args) {#创建一个 Scanner 对象Scanner scanner new Scanner(System.in);System.…

AI Agents系列之构建多智能体系统

&#x1f9e0; 向所有学习者致敬&#xff01; “学习不是装满一桶水&#xff0c;而是点燃一把火。” —— 叶芝 我的博客主页&#xff1a; https://lizheng.blog.csdn.net &#x1f310; 欢迎点击加入AI人工智能社区&#xff01; &#x1f680; 让我们一起努力&#xff0c;共创…

04.Spring 框架注解体系详解

Spring 框架注解体系详解 本文详细介绍 Spring、Spring Boot 及 Spring Cloud 中常用注解的用途、生命周期及使用方式&#xff0c;帮助开发者更深入理解 Spring 注解驱动编程模式。 参考来源&#xff1a;Spring、SpringMVC、SpringBoot、SpringCloud 框架常用注解说明 目录 注…

手撕STL——vector

目录 引言 1&#xff0c;了解 STL 中的 vector 2&#xff0c;先来一个简易版跑起来 2_1&#xff0c;构造函数 2_2&#xff0c;扩容reserve&#xff08;&#xff09; 2_3&#xff0c;push_back&#xff08;&#xff09; 2_4&#xff0c;pop_back&#xff08;&#xff09; …

优恩-具备浪涌保护功能的固态继电器UNRD0610-无触点开关器件‌

MOSFET固态继电器 : 最高负载电压&#xff1a;60V 最大负载电流&#xff1a;10A 快速响应时间&#xff1a;≤1ms 低驱动电流&#xff1a;≤10mA 高绝缘性&#xff0c;输入输出间隔离电压&#xff1a;AC3000V 耐脉冲浪涌冲击能力强 符合IEC 61000-4-2 ESD标准&#xff1a…

Kaamel隐私与安全分析报告:Microsoft Recall功能评估与风险控制

本报告对Microsoft最新推出的Recall功能进行了全面隐私与安全分析。Recall是Windows 11 Copilot电脑的专属AI功能&#xff0c;允许用户以自然语言搜索曾在电脑上查看过的内容。该功能在初次发布时因严重隐私和安全问题而备受争议&#xff0c;后经微软全面重新设计。我们的分析表…

Kotlin协程Semaphore withPermit约束并发任务数量

Kotlin协程Semaphore withPermit约束并发任务数量 import kotlinx.coroutines.* import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.launch import kotlinx.coroutines.runBlockingfun main() {val permits 1 /…

鸿蒙语言基础

准备工作 去鸿蒙官网下载开发环境 点击右侧预浏览&#xff0c;刷新和插销按钮&#xff0c;插销表示热更新&#xff0c;常用按钮。 基础语法 string number boolean const常量 数组 let s : string "1111"; console.log("string", s);let n : number …

C++数据结构与二叉树详解

前言&#xff1a; 在C编程的世界里&#xff0c;数据结构是构建高效程序的基石&#xff0c;而二叉树则是其中最优雅且应用广泛的数据结构之一。本文将带你深入理解二叉树的本质、实现与应用&#xff0c;助你在算法设计中游刃有余。 一、二叉树的基本概念 1. 什么是二叉树 二叉树…

浅析数据库面试问题

以下是关于数据库的一些常见面试问题: 一、基础问题 什么是数据库? 数据库是按照数据结构来组织、存储和管理数据的仓库。SQL 和 NoSQL 的区别是什么? SQL 是关系型数据库,使用表结构存储数据;NoSQL 是非关系型数据库,支持多种数据模型(如文档型、键值对型等)。什么是…

piamon实战-- 如何使用 Paimon 的 Java API 实现数据的点查

简介 Apache Paimon(原 Flink Table Store)是一款基于流批一体架构的 ​​高性能数据湖存储框架​​,支持低延迟的数据更新、实时查询和高效的键值点查(Point Lookup)。 本文将深入解析 Paimon 的点查机制,并通过 Java API 代码案例演示如何实现数据的点查功能。 一、Pai…

社交媒体时代的隐私忧虑:聚焦Facebook

在数字化时代&#xff0c;社交媒体平台已成为人们日常生活的重要组成部分。Facebook作为全球最大的社交媒体之一&#xff0c;拥有数十亿用户&#xff0c;其对个人隐私的影响和忧虑也日益凸显。本文将探讨社交媒体时代下&#xff0c;尤其是Facebook平台上的隐私问题。 数据收集…

问题:el-tree点击某节点的复选框由半选状态更改为全选状态以后,点击该节点展开,懒加载出来子节点数据以后,该节点又变为半选状态

具体问题场景&#xff1a; 用户点击父节点复选框将其从半选变为全选&#xff08;此时子节点尚未加载&#xff09;。 点击节点展开触发懒加载&#xff0c;加载子节点。 子节点加载后&#xff0c;组件重新计算父节点状态&#xff0c;发现并非所有子节点被选中&#xff0c;因此父节…

FastGPT安装前,系统环境准备工作?

1.启用适用于 Linux 的 Windows 子系统 方法一&#xff1a;打开控制面板 -> 程序 -> 启用或关闭Windows功能->勾选 “适用于Linux的Vindows子系统” 方法二&#xff1a;以管理员身份打开 PowerShell&#xff08;“开始”菜单 >“PowerShell” >单击右键 >“…

网页端调用本地应用打开本地文件(PDF、Word、excel、PPT)

一、背景原因 根据浏览器的安全策略&#xff0c;在网页端无法直接打开本地文件&#xff0c;所以需要开发者曲线救国。 二、实现步骤 前期准备&#xff1a; 确保已安装好可以打开文件的应用软件&#xff0c;如&#xff0c;WPS&#xff1b; 把要打开的文件统一放在一个文件夹&am…

EnlightenGAN:低照度图像增强

简介 简介:记录如何使用EnlightenGAN来做低照度图像增强。该论文主要是提供了一个高效无监督的生成对抗网络,通过全球局部歧视器结构,一种自我调节的感知损失融合,以及注意机制来得到无需匹配的图像增强效果。 论文题目:EnlightenGAN: Deep Light Enhancement Without P…

010数论——算法备赛

数论 模运算 一般求余都是对正整数的操作&#xff0c;如果对负数&#xff0c;不同编程语言结果可能不同。 C/javapythona>m,0<a%m<m-1 a<m,a%ma~5%32~-5%3 -21(-5)%(-3) -2~5%(-3)2-1正数&#xff1a;&#xff08;ab&#xff09;%m((a%m)(b%m))%m~正数&#xff…