Redis设计与实现之整数集合

目录

一、内存映射数据结构

二、整数集合

1、整数集合的应用

2、数据结构和主要操作

3、intset运行实例

创建新intset

添加新元素到 intset

添加新元素到 intset(不需要升级)

添加新元素到 intset (需要升级)

4、升级

升级实例

5、关于升级

第一,从较短整数到较长整数的转换,并不会更改元素里面的值。

第二,集合编码元素的方式,由元素中长度最大的那个值来决定。

6、 关于元素移动

7、 其他操作

读取

写入

删除

降级

三、小结


一、内存映射数据结构

虽然内部数据结构非常强大,但是创建一系列完整的数据结构本身也是一件相当耗费内存的工作,当一个对象包含的元素数量并不多,或者元素本身的体积并不大时,使用代价高昂的内部数据结构并不是最好的办法。

为了解决这一问题, Redis在条件允许的情况下,会使用内存映射数据结构来代替内部数据结构。

内存映射数据结构是一系列经过特殊编码的字节序列,创建它们所消耗的内存通常比作用类似的内部数据结构要少得多,如果使用得当,内存映射数据结构可以为用户节省大量的内存。

不过,因为内存映射数据结构的编码和操作方式要比内部数据结构要复杂得多,所以内存映射数据结构所占用的 CPU时间会比作用类似的内部数据结构要多。

这一部分将对 Redis目前正在使用的两种内存映射数据结构进行介绍。

二、整数集合

整数集合( intset)用于有序、无重复地保存多个整数值,它会根据元素的值,自动选择该用什么长度的整数类型来保存元素。

举个例子,如果在一个 intset里面,最长的元素可以用 int16_t 类型来保存,那么这个 intset的所有元素都以 int16_t 类型来保存。

另一方面,如果有一个新元素要加入到这个 intset,并且这个元素不能用 int16_t 类型来保存——比如说,新元素的长度为 int32_t ,那么这个 intset就会自动进行“升级”:先将集合中现有的所有元素从 int16_t 类型转换为 int32_t 类型,接着再将新元素加入到集合中。

根据需要, intset可以自动从 int16_t 升级到int32_t 或int64_t ,或者从 int32_t 升级到int64_t 。

1、整数集合的应用

Intset是集合键的底层实现之一,如果一个集合:

1.只保存着整数元素;

2.元素的数量不多;

那么 Redis就会使用 intset来保存集合元素。

2、数据结构和主要操作

以下是intset.h/intset 类型的定义:

typedef structintset {//保存元素所使用的类型的长度uint32_t encoding;//元素个数uint32_t length;//保存元素的数组int8_tcontents[];} intset;

encoding 的值可以是以下三个常量的其中一个(定义位于 intset.c ) :

#define INTSET_ENC_INT16 (sizeof(int16_t))#define INTSET_ENC_INT32 (sizeof(int32_t))#define INTSET_ENC_INT64 (sizeof(int64_t))

contents 数组是实际保存元素的地方,数组中的元素有以下两个特性:

•没有重复元素;

•元素在数组中从小到大排列;

contents 数组的int8_t类型声明比较容易让人误解,实际上, intset并不使用 int8_t类型来保存任何元素,结构中的这个类型声明只是作为一个占位符使用:在对 contents 中的元素进行读取或者写入时,程序并不是直接使用 contents 来对元素进行索引,而是根据 encoding的值,对 contents 进行类型转换和指针运算,计算出元素在内存中的正确位置。在添加新元素,进行内存分配时,分配的容量也是由 encoding 的值决定。

下表列出了处理 intset的一些主要操作,以及这些操作的算法复杂度:

3、intset运行实例

让我们跟踪一个 intset的创建和添加过程,籍此了解 intset的运作方式。

创建新intset

intset.c/intsetNew 函数创建一个新的 intset,并为它设置初始化值:

intset*is=intsetNew();// intset->encoding = INTSET_ENC_INT16;// intset->length 0;// intset->contents = [];

注意encoding 使用INTSET_ENC_INT16 作为初始值。

添加新元素到 intset

创建intset之后,就可以对它添加新元素了。

添加新元素到 intset的工作由 intset.c/intsetAdd 函数完成,它需要处理以下三种情况:

1.元素已存在于集合,不做动作;

2.元素不存在于集合,并且添加新元素并不需要升级;

3.元素不存在于集合,但是要在升级之后,才能添加新元素;

并且,intsetAdd 需要维持 intset->contents 的以下性质:

1.确保数组中没有重复元素;

2.确保数组中的元素按从小到大排序;

整个intsetAdd 的执行流程可以表示为下图:

以下两个小节分别演示添加操作在升级和不升级两种情况下的执行过程。

添加新元素到 intset(不需要升级)

如果 intset现有的编码方式适用于新元素,那么可以直接将新元素添加到 intset,无须对 intset进行升级。

以下代码演示了将三个 int16_t 类型的整数添加到集合的过程,以及在添加过程中,集合的状 态:

intset*is=intsetNew();intsetAdd(is, 10,NULL);// is->encoding = INTSET_ENC_INT16;// is->length = 1;// is->contents = [10];intsetAdd(is, 5,NULL);// is->encoding = INTSET_ENC_INT16;// is->length = 2;// is->contents = [5, 10];intsetAdd(is, 12, NULL);// is->encoding = INTSET_ENC_INT16;// is->length = 3;// is->contents = [5, 10, 12]

因为添加的三个元素都可以表示为 int16_t ,因此 is->encoding 一直都是 INTSET_ENC_INT16 。

另一方面,is->length 和 is->contents 的值则随着新元素的加入而被修改。

添加新元素到 intset (需要升级)

当要添加新元素到 intset ,并且 intset 当前的编码并不适用于新元素的编码时,就需要对 inset 进行升级。

以下代码演示了带升级的添加操作的执行过程:

intset *is = intsetNew();
intsetAdd(is, 1, NULL);
// is->encoding = INTSET_ENC_INT16;
// is->length = 1;
// is->contents = [1];
intsetAdd(is, 65535, NULL);
// is->encoding = INTSET_ENC_INT32;
// is->length = 2;
// is->contents = [1, 65535];
intsetAdd(is, 70000, NULL);
// is->encoding = INTSET_ENC_INT32;
// is->length = 3;
// is->contents = [1, 65535, 70000];
intsetAdd(is, 4294967295, NULL);
// is->encoding = INTSET_ENC_INT64;
// is->length = 4;
// is->contents = [1, 65535, 70000, 4294967295];
// 所有值使用 int16_t 保存
// 升级
// 所有值使用 int32_t 保存
// 升级
// 所有值使用 int64_t 保存

在添加 65535 和 4294967295 之后,encoding 属性的值,以及 contents 数组保存值的方式, 都被改变了。

4、升级

在添加新元素时,如果 intsetAdd 发现新元素不能用现有的编码方式来保存,它就会将升级集

合和添加新元素的任务转交给 intsetUpgradeAndAdd 来完成:

intsetUpgradeAndAdd 需要完成以下几个任务:

  1. 对新元素进行检测,看保存这个新元素需要什么类型的编码;

  2. 将集合encoding属性的值设置为新编码类型,并根据新编码类型,对整个contents数 组进行内存重分配。

  3. 调整contents数组内原有元素在内存中的排列方式,让它们从旧编码调整为新编码。

  4. 将新元素添加到集合中。

整个过程中,最复杂的就是第三步,让我们用一个例子来理解这个步骤。 

升级实例

假设有一个 intset ,里面包含三个用 int16_t 方式保存的数值,分别是 1 、2 和 3 ,它的结 构如下: 

intset->encoding = INTSET_ENC_INT16;
intset->length = 3;
intset->contents = [1, 2, 3];

其中,intset->contents 在内存中的排列如下:

bit     0  15  31  47 
value   | 1 | 2 | 3 |

现在,我们要要将一个长度为 int32_t 的值 65535 加入到集合中,intset 需要执行以下步骤:

1. 将encoding属性设置为INTSET_ENC_INT32。
2. 根据encoding属性的值,对contents数组进行内存重分配。

重分配完成之后,contents 在内存中的排列如下:

bit     0  15  31 47  63  95  127 
value   | 1 | 2 | 3 | ? | ? | ? |

3.因为原来的 3 个 int16_t 值还“挤在”contents 前面的 48 个位里,所以程序需要对它们

进行移动和类型转换,从而让它们适应集合的新编码方式。 首先是移动 3 :

 接着移动2:

 最后,移动 1 :

4.最后,将新值 65535 添加到数组:

将 intset->length 设置为 4 。

至此,集合的升级和添加操作完成,现在的 intset 结构如下:

intset->encoding = INTSET_ENC_INT32;
intset->length = 4;
intset->contents = [1, 2, 3, 65535];

5、关于升级

关于升级操作,还有两点需要提醒一下:

第一,从较短整数到较长整数的转换,并不会更改元素里面的值。

在 C 语言中,从长度较短的带符号整数到长度较长的带符号整数之间的转换(比如从 int16_t 转换为 int32_t )总是可行的(不会溢出)、无损的。

另一方面,从较长整数到较短整数之间的转换可能是有损的(比如从 int32_t 转换为 int16_t )。

因为 intset 只进行从较短整数到较长整数的转换(也即是,只“升级” ,不“降级” ),因此, “升 级”操作并不会修改元素原有的值。

第二,集合编码元素的方式,由元素中长度最大的那个值来决定。

就像前面演示的例子一样,当要将一个 int32_t 编码的新元素添加到集合时,集合原有的所有 int16_t 编码的元素,都必须转换为 int32_t 。

尽管这个集合真正需要用 int32_t 长度来保存的元素只有一个,但整个集合的所有元素都必须 转换为这种类型。

6、 关于元素移动

在进行升级的过程中,需要对数组内的元素进行“类型转换”和“移动”操作。

其中,移动不仅出现在升级(intsetUpgradeAndAdd)操作中,还出现其他对 contents 数组内 容进行增删的操作上,比如 intsetAdd 和 intsetRemove ,因为这种移动操作需要处理 intset 中的所有元素,所以这些函数的复杂度都不低于 O(N) 。

7、 其他操作

以下是一些关于 intset 其他操作的讨论。

读取

有两种方式读取 intset 的元素,一种是 _intsetGet ,另一种是 intsetSearch :
• _intsetGet 接受一个给定的索引 pos ,并根据 intset->encoding 的值进行指针运算,

计算出给定索引在 intset->contents 数组上的值。
• intsetSearch 则使用二分查找算法,判断一个给定元素在 contents 数组上的索引。

写入

除了前面介绍过的 intsetAdd 和 intsetUpgradeAndAdd 之外,_intsetSet 也对集合进行写 入操作:它接受一个索引 pos ,以及一个 new_value ,将 contents 数组 pos 位置的值设为 new_value 。

删除

删除单个元素的工作由 intsetRemove 操作,它先调用 intsetSearch 找到需要被删除的元素 在 contents 数组中的索引,然后使用内存移位操作,将目标元素从内存中抹去,最后,通过 内存重分配,对 contents 数组的长度进行调整。

降级

Intset 不支持降级操作。

Intset 定位为一种受限的中间表示,只能保存整数值,而且元素的个数也不能超过 redis.h/REDIS_SET_MAX_INTSET_ENTRIES (目前版本值为 512 )这些条件决定了它被保 存的时间不会太长,因此对它进行太复杂的操作,没有必要。

当然,如果内存确实十分紧张的话,给 intset 添加降级功能也是可以实现的,不过这可能会让 intset 的代码增长一倍。

三、小结

  • Intset 用于有序、无重复地保存多个整数值,它会根据元素的值,自动选择该用什么长度的整数类型来保存元素。

  • 当一个位长度更长的整数值添加到 intset 时,需要对 intset 进行升级,新 intset 中每个 元素的位长度都等于新添加值的位长度,但原有元素的值不变。

  • 升级会引起整个 intset 进行内存重分配,并移动集合中的所有元素,这个操作的复杂度 O(N) 

  • Intset 只支持升级,不支持降级。

  • Intset 是有序的,程序使用二分查找算法来实现查找操作,复杂度为 O(lgN) 。

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

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

相关文章

GZ015 机器人系统集成应用技术样题4-学生赛

2023年全国职业院校技能大赛 高职组“机器人系统集成应用技术”赛项 竞赛任务书(学生赛) 样题4 选手须知: 本任务书共 25页,如出现任务书缺页、字迹不清等问题,请及时向裁判示意,并进行任务书的更换。参赛队…

Flutter在Android Studio上创建项目与构建模式

一、安装插件 1、前提条件,安装配置好Android Studio环境 2、安装Flutter和Dart插件 Linux或者Windows平台: 1)、打开File > Settings。 2)、在左侧列表中,选择"Plugins"右侧上方面板选中 "Market…

vue-element-admin如何把mock换成使用真实后台接口

1)修改vue.config.js文件 use strict const path require(path) const defaultSettings require(./src/settings.js)function resolve(dir) {return path.join(__dirname, dir) }const name defaultSettings.title || vue Element Admin // page title// If you…

Vue3-16-【v-model】 表单数据绑定

作用描述 v-model 指令,实现了 表单输入组件的值 与 js 中的变量的值的绑定关系。 当我们在页面上执行输入动作时,js中变量的值也会同步发生变化。表单不仅仅局限于输入框,其他的如 : 单选按钮,复选框,下拉…

数据结构面试题和题目解析

以下是一些数据结构的面试题和解析: 1. 什么是链表? 链表是一种线性数据结构,由一系列节点组成,每个节点包含数据部分和指向下一个节点的指针。链表的主要优点是插入和删除操作比较方便,但访问链表中的元素不如访问数组…

【INTEL(ALTERA)】 quartus SignalTap 逻辑分析器 – Nios® II 插件 无法检测 Nios® II/f 处理器内核

说明 使用 Nios II 插件将 Nios II/f 处理器内核节点添加到 SignalTap 逻辑分析器时,在 英特尔 Quartus Prime Pro Edition 软件 23.3 版中可能会出现此问题。 错误消息: 无法完成“添加带插件的节点”命令,因为在当前设计中找不到所选 IP。…

IDEA之设置项目包的结构层级为eclipse默认样式

idea默认项目包的结构层级如下: 想修改成eclipse默认的那种样式,设置步骤如下: 1.点击下图中红框图标进行设置 2.选择 Tree Appearance,取消勾选 Compact Middle Packages 3.勾选红框里的两个选项,Flatten Packages 和 Hide Empty Middle Pa…

DL Homework 11

由于好多同学问我要代码,但这两天光顾着考四六级了,所以只能今天熬夜先给赶出来,第一题先搁置,晚点补上,先写第二题 习题6-4 推导LSTM网络中参数的梯度, 并分析其避免梯度消失的效果 习题6-3P 编程实现…

KMP算法, 什么是KMP算法 ,暴力匹配 ,KMP算法实现

文章目录 KMP算法什么是KMP算法暴力匹配KMP算法实现 KMP算法 什么是KMP算法 KMP是Knuth、Morris和Pratt首字母的缩写,KMP也是由这三位学者发明(1977年联合发表论文)。 KMP主要应用在字符串的匹配,是一个解决模式串在文本串是否…

文章解读与仿真程序复现思路——电网技术EI\CSCD\北大核心《耦合碳-绿证-消纳量市场的日前电量市场交易交互式优化》

这个标题描述了一种优化模型或算法,用于在日前电量市场中耦合碳排放权市场、可再生能源绿色证书市场和消纳量市场进行交易的交互式优化。我将解析标题的关键词和概念: 日前电量市场:指的是电力市场中进行短期调度和交易的市场,其…

Idea maven打包时 报错 illegalArgumentException: Malformed \uxxxx encoding 解决方法

1 改变打包命令重新打包 在maven打包命令上加入 -e -X 2 找到报错类和方法 可以看到是 java.util.Properties#loadConvert类方法中有个throw new IllegalArgumentException( "Malformed \\uxxxx encoding."),在此打断点 3 以Debug方式重新运行maven…

SLAM算法与工程实践——相机篇:传统相机使用(1)

SLAM算法与工程实践系列文章 下面是SLAM算法与工程实践系列文章的总链接,本人发表这个系列的文章链接均收录于此 SLAM算法与工程实践系列文章链接 下面是专栏地址: SLAM算法与工程实践系列专栏 文章目录 SLAM算法与工程实践系列文章SLAM算法与工程实践…

死锁的预防、避免、检测和消除

一、预防死锁 1. 破坏互斥条件 2. 破坏不剥夺条件 3.破坏请求和保持条件 4.破坏循环等待条件 二、避免死锁 避免死锁的一种方法是使用银行家算法,它涉及到安全序列的概念。银行家算法是一种资源分配和死锁避免的算法,它确保系统能够分配资源而不会导致死…

STM32迪文屏图标控件保姆级教程

要主图的去末尾,末尾福利图在等着你~~~ 文章目录 前言 开发环境 二、使用步骤 1.添加图标控件 2.设置图标属性 3.图标库ICL文件生成 4.单片机程序编写 容易踩得坑 一、前言 本篇文章主要介绍了在DGBUS平台上使用图标变量的步骤。首先需要在DGBUS中添加一个图标变量控…

linux(centos7)mysql8.0主从集群搭建(两台机器)

docker安装:(转载)centos7安装Docker详细步骤(无坑版教程)-CSDN博客 环境信息 主数据库服务器:192.168.1.10 从数据库服务器:192.168.1.11 1. mysql8.0镜像下载 docker pull mysql:8.0.23 2.创建docke…

瑞萨单片机学习:RA4M3单片机 BOOTloader升级 跳转到主程序 主程序无法执行问题

背景: 使用瑞萨的RA4M3单片机编写BOOT引导程序进行测试,在BOOT程序跳转到主程序时,主程序无法执行。本文介绍了问题的定位和解决方法。 运行开发环境介绍 硬件环境 RA4M3 官方开发板 J-LINK V11 开发板自带 软件开发环境 e2 studio VSCODE…

oracle怎么导入dmp文件??????

目录 oracle怎么导入dmp文件?????? 先看: 方法一:【推荐】 winR输入 输入: 检验: 导入成功! 方法二: 直接在 PLSQL Developer…

插入排序:直接插入排序 希尔排序

插入排序: 假设红竖线前的元素全部排好序,红线后面的数即为要插入的数据,红线依次往后移,假设end为排好序的最后一个数字,end1即为要插入的数字,一次插入时,end与要插入的数字依次比较&#xf…

Axure元件库的使用

1.基本元件库 1.1Axure的画布范围 Axure是一个绘制项目原型图的软件,它里面的基本原件有: 1.1元件的呈现范围 首先我们要了解基本元件的作用范围在哪里? 浏览效果: 可以看出当我们的基本元件放在画布区域内是可以完全呈现出来…

从0创建并部署一个网页到服务器

创建一个页面 1 下载node.js 下载VScode 2 在Windows下找一个路径新建一个文件夹 例如:D:\study_project\PersonalWeb 3 VSCodee中打开文件夹 4 Windows下 管理员身份打开命令提示符,执行npm install -g vue/cli 5 VSCode下打开终端,执…