redis笔记-数据结构
zset
zset一方面它是一个 set,保证了内部value 的唯一性,另一方面它可以给每个 value 赋予一个 score,代表这个 value 的排序权重。
zset的底层是由字典和跳表实现。
字典主要用来存储value和score的对应关系。跳表这个数据结构主要用来提供zset按照score排序的功能、指定score范围获取value列表的功能。
struct zset {dict *dict; // all values value=>scorezskiplist *zsl;
}
跳表
跳表数据结构包含的属性:
- header:表示kv head的地址
- maxLevel:最高层级
struct zskiplist {zslnode* header; // 跳跃列表头指针int maxLevel; // 跳跃列表当前的最高层map<string, zslnode*> ht; // hash 结构的所有键值对
}
以下就是跳表的示意图:
每一个kv按照score有序排列的,不同的 kv 层高可能不一样,层数越高的 kv 越少。也就是不同 kv 的level[ ]长度不一样。同一层的 kv 会使用指针串起来。每一个层元素的遍历都是从 kv header 出发。
为什么需要这么设计呢?如果只有一层,也就是一个双链表,所有元素按照score排序,我们如果想要快速定位到某个score,我们就需要从头指针依次遍历,时间复杂度为O(n)。如果我们设计成跳表这个多层结构,效果类似于二分查找,时间复杂度为O(lg(n))。

每一个结点包含的属性如下:
struct zslnode {string value;double score;zslforward*[] level; // 多层连接指针zslnode* backward; // 回溯指针
}
struct zslforward {zslnode* forward;long span; // 跨度
}
查找
如果当前节点的下一个节点包含的值比目标元素值小,则继续向右查找。如果下一个节点的值比目标值大,就转到当前层的下一层去查找。

比如说要查找21,当查找到6这个结点时,6比21小,继续向右查找,因为此时在6结点level[3],level[3]的下一个结点是nil,不符合,那么就转到6的level[2],level[2]的下一个结点是(6->level[2].forward)25,比目标值大,继续转到6的level[1],level[1]的下一个结点是9,9比21小,继续向右查找……
注意: zset 的排序元素不只看 score 值,如果score 值相同还需要再比较 value 值 (字符串比较)。为了防止如果每个元素的score值都相同的话,查找时间复杂度变成O(n)。
插入
当客户端使用 zset add 命令时,redis底层插入元素的过程:

zskiplistNode *zslInsert(zskiplist *zsl, double score, robj *obj) {// 存储搜索路径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] = 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 &&compareStringObjects(x->level[i].forward->obj,obj) < 0))) {// 累计搜索路径中经过各结点的跨度。也就是最后查询到的元素的rank值。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,obj);// 重排一下前向指针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++;}// 重排一下后向指针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;
}
- 通过 kv head 这个结点获得最高层的结点地址。
- 逐层降级寻找目标结点,所经过的结点存储在搜索路径update[]数组中。
- 创建一个新结点。
- 为这个结点算一个随机层数,表示这个结点的level[]最大是第几层。
- 遍历update数组,为这个新创建的结点的每层level设置好前后指针。
删除
删除过程和插入过程类似,都需先把这个「搜索路径」找出来。然后对于每个层的相关节点都重排一下前向后向指针就可以了。同时还要注意更新一下最高层数 maxLevel。
更新
当我们调用 zadd 方法时,如果对应的 value 不存在,那就是【插入过程】。
如果这个value 已经存在了,只是调整一下 score 的值,
- 假设这个新的score 值不会带来排序位置上的改变,那么就不需要调整位置,直接修改元素的 score 值就可以了。
- 如果新的score值让排序位置改变了,那就要调整位置。如何调整位置?直接将这个元素删了,然后插入。这一点可以优化,因为如果直接删,然后再插入,需要两次路径搜索,对于改变score值后仍不需要调整位置的情况就可以优化。
字典
dict数据结构可以应用在:
- hash 结构的数据
- 整个 Redis 数据库的所有 key 和 value
- 带过期时间的 key 集合
- zset 集合中存储 value 和 score 值的映射关系
struct RedisDb {dict* dict; // all keys key=>valuedict* expires; // all expired keys key=>long(timestamp)
}
struct zset {dict *dict; // all values value=>scorezskiplist *zsl;
}
dict数据结构包含的属性:
struct dict {...dictht ht[2]; // 两个dictht结构,也就是两个hashtable
}
struct dictht {dictEntry** table; // 二维,第一维是数组,第二维是链表。数组中存储的是第二维链表的第一个元素的指针。long size; // 第一维数组的长度long used; // hash 表中的元素个数...
}
struct dictEntry {void* key;void* val;dictEntry* next; // 链接下一个 entry
}
rehash
rehash条件:
Dict在每次新增键值对时都会检查负载因子(LoadFactor = used/size) ,满足以下两种情况时会触发哈希表扩容:
哈希表的 LoadFactor >= 1,并且服务器没有执行 BGSAVE 或者 BGREWRITEAOF 等后台进程;
哈希表的 LoadFactor > 5 ;
rehash过程:(渐进式rehash)
-
计算新hash表的realeSize,值取决于当前要做的是扩容还是收缩:
- 如果是扩容,则新size为第一个大于等于dict.ht[0].used + 1的2^n
- 如果是收缩,则新size为第一个大于等于dict.ht[0].used的2^n (不得小于4)
-
按照新的realeSize申请内存空间,创建dictht,并赋值给dict.ht[1]
-
设置dict.rehashidx = 0,标示开始rehash
-
将dict.ht[0]中的每一个dictEntry都rehash到dict.ht[1](注意,这里不是一次性就将所有entry移到ht[1]中。)
每次执行新增、查询、修改、删除操作时,都检查一下dict.rehashidx是否大于-1,如果是则将dict.ht[0].table[rehashidx]的entry链表rehash到dict.ht[1],并且将rehashidx++。直至dict.ht[0]的所有数据都rehash到dict.ht[1]。—渐进式哈希
-
将dict.ht[1]赋值给dict.ht[0],给dict.ht[1]初始化为空哈希表,释放原来的dict.ht[0]的内存。
-
将rehashidx赋值为-1,代表rehash结束。
-
在rehash过程中,新增操作,则直接写入ht[1],查询、修改和删除则会在dict.ht[0]和dict.ht[1]依次查找并执行。这样可以确保ht[0]的数据只减不增,随着rehash最终为空。
rehash时机:
rehash将ht[0]的数据搬迁到ht[1]是在客户端执行命令中时发生的。也就是渐进性哈希。如果客户端闲下来,redis则通过定时任务对字典进行搬迁。
string
Redis 中的字符串是可以修改的字符串,在内存中它是以字节数组的形式存在的。这个字符串叫「SDS」,也就是 Simple Dynamic String。它的结构是一个带长度信息的字节数组。
SDS
struct SDS<T> {T capacity; // 数组容量T len; // 数组长度byte flags; // 特殊标识位,不理睬它byte[] content; // 数组内容
}

如上图所示,capacity 表示所分配数组的长度,len 表示字符串的实际长度。
embstr vs raw
Redis 的字符串有两种存储方式,在长度特别短时,使用 emb 形式存储 (embeded),当长度超过 44 时,使用 raw 形式存储。
所有redis对象都有下面这个结构头,包括我们现在研究的SDS对象也会有这个对象头。
struct RedisObject {int4 type; // 4bits 不同的对象具有不同的类型int4 encoding; // 4bits 同一个类型的 type 会有不同的存储形式encoding(4bit),int24 lru; // 24bits int32 refcount; // 4bytes 每个对象都有个引用计数,当引用计数为零时,对象就会被销毁,内存被回收。void *ptr; // 8bytes,64-bit system ptr 指针将指向对象内容 (body) 的具体存储位置。
} robj;
这样一个 RedisObject 对象头需要占据 16 字节的存储空间。
我们再看 SDS 结构体的大小:
struct SDS {int8 capacity; // 1byteint8 len; // 1byteint8 flags; // 1bytebyte[] content; // 内联数组,长度为 capacity
}
一个SDS对象头至少是3字节,那么一整个SDS对象至少是3+16=19字节,也就是说分配一个字符串最小空间占用为19字节。

- embstr 存储形式将 RedisObject 对象头和 SDS 对象连续存在一起,使用 malloc 方法一次分配。
- raw 存储形式需要两次malloc,两个对象头在内存地址上一般是不连续的。
内存分配器 jemalloc/tcmalloc 等分配内存大小的单位都是 2、4、8、16、32、64 等等,为了能容纳一个完整的 embstr 对象,**jemalloc 最少会分配 32 字节的空间,如果字符串再稍微长一点,那就是 64 字节的空间。**如果总体超出了 64 字节,Redis 认为它是一个大字符串,不再使用 emdstr 形式存储,而该用 raw 形式。
也就是如果分配的是64字节的空间,那么扣掉SDS对象头和redis对象头,剩下64 - 19 = 45字节给字符串,而字符串是以字节\0 结尾的,因此字符串长度最大为44字节,如果超过44字节,那么用raw形式存储。
oc/tcmalloc 等分配内存大小的单位都是 2、4、8、16、32、64 等等,为了能容纳一个完整的 embstr 对象,**jemalloc 最少会分配 32 字节的空间,如果字符串再稍微长一点,那就是 64 字节的空间。**如果总体超出了 64 字节,Redis 认为它是一个大字符串,不再使用 emdstr 形式存储,而该用 raw 形式。
也就是如果分配的是64字节的空间,那么扣掉SDS对象头和redis对象头,剩下64 - 19 = 45字节给字符串,而字符串是以字节\0 结尾的,因此字符串长度最大为44字节,如果超过44字节,那么用raw形式存储。
参考
《Redis深度历险:核心原理和应用实践》
https://www.jianshu.com/p/09c3b0835ba6
相关文章:
redis笔记-数据结构
zset zset一方面它是一个 set,保证了内部value 的唯一性,另一方面它可以给每个 value 赋予一个 score,代表这个 value 的排序权重。 zset的底层是由字典和跳表实现。 字典主要用来存储value和score的对应关系。跳表这个数据结构主要用来提…...
webpack的常见配置
Webpack 是一个现代 JavaScript 应用的模块打包工具,用于将项目中的多个文件和依赖打包成浏览器可以识别的文件,通常是一个或多个 JavaScript、CSS 或其他静态资源的 bundle(将多个模块或文件合并成一个或几个文件的过程,这些合并…...
text-embedding-ada-002;BGE模型;M3E模型是Moka Massive Mixed Embedding;BERT
目录 text-embedding-ada-002 一、模型概述 二、模型功能 三、模型特点 四、模型应用 五、模型优势 BGE模型 一、模型背景与特点 二、模型性能与表现 三、模型迭代与发展 M3E模型是Moka Massive Mixed Embedding 一、基本信息 二、技术特点 三、应用场景 四、性能…...
WebRTC 环境搭建
主题 本文主要描述webrtc开发过程中所需的环境搭建 环境: 运行环境:ubuntu 20.04 Node.js环境搭建 安装编译 Node.js 所需的依赖包: sudo apt-get update sudo apt-get install -y build-essential libssl-dev 下载 Node.js 源码: curl -sL htt…...
FastHTML快速入门:http方法,CSS文件和内联样式,其他静态媒体文件位置
HTTP方法 FastHTML通过函数名与HTTP方法进行匹配。到目前为止,我们定义的URL路由都是针对HTTP GET方法的,这是网页最常见的方法。 表单提交通常作为HTTP POST发送。在处理更动态的网页设计时,也就是所谓的单页应用(SPA࿰…...
项目管理和研发管理中的痛点及其解决方案
在现代企业中,研发管理和项目管理面临着多重挑战,包括资源配置不当、沟通不畅、目标不明确、进度控制困难等。这些痛点不仅影响项目的顺利推进,还可能导致企业在市场竞争中处于劣势。尤其是在资源配置不当方面,企业往往难以合理分…...
机器学习(基础1)
数据集 sklearn玩具数据集 数据量小,数据在sklearn库的本地,只要安装了sklearn,不用上网就可以获取 sklearn现实世界数据集 数据量大,数据只能通过网络获取(为国外数据集,下载需要梯子) skle…...
我谈维纳(Wiener)复原滤波器
Rafael Gonzalez的《数字图像处理》中,图像复原这章内容几乎全错。上篇谈了图像去噪,这篇谈图像复原。 图像复原也称为盲解卷积,不处理点扩散函数(光学传递函数)的都不是图像复原。几何校正不属于图像复原,…...
怎么看真假国企啊?怎么识别假冒国企的千层套路?
一、怎么看真假国企啊? 1.使用具有迷惑性的名称:假冒国企往往在名称中使用“中国”、“中”、“国”等字样,或与知名国企名称相似的字号,以增加其可信度。 2.注册资本虚高:为了显示实力,假冒国企可能会在…...
C#中break和continue的区别?
在C#编程语言中,break和continue是两个用于控制循环流程的关键字,但它们的作用和用途有所不同。 break关键字 break关键字用于立即终止它所在的最内层循环或switch语句,并跳出该循环或switch块。程序执行将继续进行循环或switch语句之后的下一…...
Linux部署nginx访问文件403
问题描述:在linux服务器上通过nginx部署,访问文件403 新配置了一个用户来部署服务,将部署文件更新到原有目录下,结果nginx访问403 原因:没有配置文件的读写权限,默认不可读写,nginx无法访问到文…...
华为OD机试 - 数字排列 - 深度优先搜索dfs算法(Python/JS/C/C++ 2024 C卷 200分)
华为OD机试 2024E卷题库疯狂收录中,刷题点这里 专栏导读 本专栏收录于《华为OD机试真题(Python/JS/C/C)》。 刷的越多,抽中的概率越大,私信哪吒,备注华为OD,加入华为OD刷题交流群,…...
Scrapy爬取heima论坛所有页面内容并保存到数据库中
前期准备: Scrapy入门_win10安装scrapy-CSDN博客 新建 Scrapy项目 scrapy startproject mySpider03 # 项目名为mySpider03 进入到spiders目录 cd mySpider03/mySpider03/spiders 创建爬虫 scrapy genspider heima bbs.itheima.com # 爬虫名为heima &#…...
Kafka参数了解
Kafka配置参数完整说明 1. 基础配置 参数名说明推荐值参考值broker.idbroker的唯一标识符每个节点唯一的整数1delete.topic.enable是否允许删除topictruetruelistenersbroker监听地址SASL_PLAINTEXT://host:9092SASL_PLAINTEXT://172.24.77.15:9092advertised.listeners对外发…...
sql专题 之 where和join on
文章目录 前言where介绍使用过滤结果集关联两个表 连接外连接内连接自然连接 使用inner join和直接使用where关联两个表的区别总结 前言 从数据库查询数据时,一张表不足以查询到我们想要的数据,更多的时候我们需要联表查询。 联表查询我们一般会使用连接…...
day12:版本控制器
版本控制 使用到的命令: ls -al查看当前目录下的文件及文件夹mkdir新建目录rm -rf递归强制删除文件夹 一、安装配置 1、下载地址 Git 2、初始配置 #用户名 git config --global user.name "自定义用户名" #邮箱(公司的联系方式--追责&…...
第四十一章 Vue之初识VueX
目录 一、引言 1.1. vuex的概念 1.2. vuex使用场景 1.3. 优势 二、创建演示项目 2.1. 构建项目步骤 2.2. 项目最终生成结构 2.3. 创建项目文件 2.3.1. App.vue 2.3.2. Son1.vue 2.3.3. Son2.vue 三、创建一个空仓库 3.1. 安装vuex 3.2. 新建仓库 3.3. 挂载仓库…...
GIT的基本使用与进阶
GIT的简单入门 一.什么是git? Git 是一个开源的分布式版本控制系统,用于跟踪文件更改、管理代码版本以及协作开发。它主要由 Linus Torvalds 于 2005 年创建,最初是为 Linux 内核开发而设计的。如今,Git 已经成为现代软件开发中…...
【Linux系统】—— 基本指令(二)
【Linux系统】—— 基本指令(二) 1 「alias」命令1.1 「ll」命令1.2 「alias」命令 2 「rmdir」指令与「rm」指令2.1 「rmdir」2.2 「rm」2.2.1 「rm」 删除普通文件2.2.2 「rm」 删除目录2.2.3 『 * 』 通配符 3 「man」 指令4 「cp」 指令4.1 拷贝普通…...
MFC工控项目实例三十实现一个简单的流程
启动按钮夹紧 密闭,时间0到平衡 进气,时间1到进气关,时间2到平衡关 检测,时间3到平衡 排气,时间4到夹紧开、密闭开、排气关。 相关代码 void CSEAL_PRESSUREDlg::OnTimer_2(UINT nIDEvent_2) {// if (nIDEvent_21 &am…...
如何实现SASM多语言支持:完整国际化配置与翻译指南
如何实现SASM多语言支持:完整国际化配置与翻译指南 【免费下载链接】SASM SASM - simple crossplatform IDE for NASM, MASM, GAS and FASM assembly languages 项目地址: https://gitcode.com/gh_mirrors/sa/SASM SASM(Simple Assembler IDE&…...
Dify向量检索精度跃升47%的秘密(重排序Pipeline低延迟部署避坑手册)
第一章:Dify重排序算法精度跃升47%的核心动因解析Dify v0.12.0 引入的重排序(Reranking)模块并非简单叠加模型,而是通过三重协同优化机制实现精度质变。核心突破在于将传统单阶段打分范式升级为「语义对齐—上下文感知—动态归一化…...
别再只调参了!深入RepVgg设计思想,用CCFF模块优化你的模型特征融合效率
深入解析CCFF模块:用RepVgg思想重构跨尺度特征融合技术 在计算机视觉领域,特征融合一直是提升模型性能的关键环节。传统方法如FPN、PANet虽然有效,但在实时性要求高的场景下往往成为计算瓶颈。今天我们要探讨的CCFF(Cross-scale C…...
3分钟搞定网易云音乐加密文件:NCMD解密工具终极指南
3分钟搞定网易云音乐加密文件:NCMD解密工具终极指南 【免费下载链接】ncmdump 项目地址: https://gitcode.com/gh_mirrors/ncmd/ncmdump 还在为网易云音乐的NCM加密文件无法在其他播放器播放而烦恼吗?今天我要为你介绍一款简单高效的音频解密神器…...
3步实现会呼吸的界面边界:让表单交互体验提升300%的动态效果
3步实现会呼吸的界面边界:让表单交互体验提升300%的动态效果 【免费下载链接】motion-primitives UI kit to make beautiful, animated interfaces, faster. Customizable. Open Source. 项目地址: https://gitcode.com/gh_mirrors/mo/motion-primitives 在现…...
Qt项目里用dxflib解析CAD图纸,遇到带圆弧的多段线(LWPOLYLINE)怎么画?
Qt项目中利用dxflib解析CAD图纸:带圆弧多段线(LWPOLYLINE)的精确绘制方案 在工业设计、建筑图纸和机械制图领域,DXF文件作为CAD数据交换的标准格式,其精确解析一直是开发者的核心挑战。当使用Qt框架结合dxflib库进行CAD可视化时,优…...
阿里云域名动态解析避坑指南:从AccessKey到API调用的完整流程
阿里云域名动态解析实战手册:从权限配置到高可用方案设计 对于拥有个人博客、家庭NAS或远程开发环境的技术爱好者而言,动态公网IP始终是个令人头疼的问题。每当ISP重新分配IP地址时,原本稳定的服务连接就会突然中断。本文将分享如何利用阿里云…...
【跟韩工学Ubuntu第5课】-第5章 网络管理:Netplan、路由与防火墙-004篇-Ubuntu Server 网络管理:进阶配置、优化与实战诊断
文章目录 Ubuntu Server 网络管理:进阶配置、优化与实战诊断 (扩容优化版 | 适配高校教学+生产实战 | 30页核心内容) 5.1 网络基础:深入理解与实践查看(扩容+优化) 一、核心概念进阶(新增计算案例+场景区分) 二、必备诊断命令(新增高频参数+中文注释) 三、IPv6 完整配…...
SpringBoot WebSocket 客户端断线重连:从心跳检测到优雅恢复
1. WebSocket与实时通信的挑战 想象一下你正在玩一款多人在线游戏,突然网络卡顿导致角色掉线,重新登录后发现之前的战斗进度全部丢失——这种糟糕体验正是WebSocket重连机制要解决的问题。WebSocket作为HTTP的"升级版",确实解决了服…...
颠覆式突破:无需模拟器,在Windows系统上直接运行Android应用的革命性方案
颠覆式突破:无需模拟器,在Windows系统上直接运行Android应用的革命性方案 【免费下载链接】APK-Installer An Android Application Installer for Windows 项目地址: https://gitcode.com/GitHub_Trending/ap/APK-Installer 想象一下,…...
