Redis设计与实现之整数集合

news2024/9/21 16:47:03

目录

一、内存映射数据结构

二、整数集合

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.coloradmin.cn/o/1316522.html

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

相关文章

25.Java程序设计-基于SSM框架的微信小程序校园求职系统的设计与实现

1. 引言 1.1 背景 介绍校园求职系统的背景,说明为什么设计这个系统以及系统的重要性。 1.2 研究目的 阐述设计基于SSM框架的微信小程序校园求职系统的目标和意义。 2. 需求分析 2.1 行业背景 分析校园求职行业的特点和需求,以及目前市场上同类系统…

Spring Boot SOAP Web 服务端和客户端

一. 服务端 1. 技术栈 JDK 1.8,Eclipse,Maven – 开发环境SpringBoot – 基础应用程序框架wsdl4j – 为我们的服务发布 WSDLSOAP-UI – 用于测试我们的服务JAXB maven 插件 – 用于代码生成 2.创建 Spring Boot 项目 添加 Wsdl4j 依赖关系 编辑pom…

化学方程式小程序

brief introduction 相信大家上中学时都会被化学方程式折腾得死去活来,尤其是配平,怎么也算不对数字。于是我写出了这款近200行的自动配平程序,这是不是你们黑暗化学中的一丝光亮呢? usage 正常化学式输入,每一种物…

智慧农业大数据可视化UI,数据展示平台(免费可视化大屏模版PS资料)

大屏幕展示方式可以实现信息的直观呈现与交互操作,使农业生产者能够一目了然地掌握有关农情、天气、土壤等数据信息,从而科学决策。智慧农业大数据可视化大屏是提升农业生产效益的一种重要工具。 现分享亩产效益指标、农业大数据可视化、农业数据展示平…

基于EasyExcel的数据导入导出

前言: 代码复制粘贴即可用,主要包含的功能有Excel模板下载、基于Excel数据导入、Excel数据导出。 根据实际情况修改一些细节即可,最后有结果展示,可以先看下结果,是否是您想要的。 台上一分钟,台下60秒&a…

QT Widget - 随便画个圆

简介 实现在界面中画一个圆, 其实目的是想画一个LED效果的圆。代码 #include <QApplication> #include <QWidget> #include <QPainter> #include <QColor> #include <QPen>class LEDWidget : public QWidget { public:LEDWidget(QWidget *pare…

前端传递参数,后端如何接收

目录 简单参数 传递方式 获取方式一 获取方式二 相关注解 实体参数 数组集合参数 传递方式 相关注解 获取方式一 获取方式二 日期参数 传递方式 相关注解 获取方式 json参数 传递方式 相关注解 获取方式 路径参数 传递方式 相关注解 获取方式 传递多个…

【Java系列】详解多线程(三)—— 线程安全(上篇)

个人主页&#xff1a;兜里有颗棉花糖 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 兜里有颗棉花糖 原创 收录于专栏【Java系列专栏】【JaveEE学习专栏】 本专栏旨在分享学习Java的一点学习心得&#xff0c;欢迎大家在评论区交流讨论&#x1f48c; 目录 一…

心理测试网站源码,知己心理React心理健康测试

源码介绍 React心理健康测试网站源码&#xff0c;帮助需要的人更好地了解自已的心理健康状态和人格特征。 React可以在Vite中启用HMR&#xff0c;并且包含了几人EsLint规则。只需要使用react antd-mobile即可 轻松部署完成。

【JAVA日志框架】JUL,JDK原生日志框架详解。

前言 Java日志体系混乱&#xff1f;Java日志框架系列&#xff0c;清晰简洁整理好整个Java的日志框架体系。第一篇&#xff0c;JDK原生日志框架——JUL。 目录 1.概述 2.日志级别 3.配置 4.继承关系 1.概述 日志框架的核心问题&#xff1a; 日志是用来记录应用的一些运行…

C++ Qt开发:Tab与Tree组件实现分页菜单

Qt 是一个跨平台C图形界面开发库&#xff0c;利用Qt可以快速开发跨平台窗体应用程序&#xff0c;在Qt中我们可以通过拖拽的方式将不同组件放到指定的位置&#xff0c;实现图形化开发极大的方便了开发效率&#xff0c;本章将重点介绍tabWidget选择夹组件与TreeWidget树形选择组件…

Swift 响应式编程:简化 KVO 观察与 UI 事件处理 | 开源日报 No.110

ReactiveX/RxSwift Stars: 23.8k License: MIT RxSwift 是 Reactive Extensions 标准的 Swift 特定实现&#xff0c;它提供了 Observable 接口来表达计算的通用抽象。该项目旨在为 Rx API 提供真正以 Swift 为先的 API&#xff0c;并允许轻松地组合异步操作和数据流。其主要功…

K8s投射数据卷

目录 一.Secret 1.secret介绍 2.secret的类型 3.创建secret 4.使用secret 环境变量的形式 volume数据卷挂载 二ConfigMap 1.创建ConfigMap的方式 2.使用ConfigMap 2.1作为volume挂载使用 2.2.作为环境变量 三.Downward API 1.以环境变量的方式实现 2.Volume挂载 一.S…

Linux 中使用 docker 安装 Elasticsearch 及 Kibana

Linux 中使用 docker 安装 Elasticsearch 及 Kibana 安装 Elasticsearch 和 Kibana安装分词插件 ik_smart 安装 Elasticsearch 和 Kibana 查看当前运行的镜像及本地已经下载的镜像&#xff0c;确认之前没有安装过 ES 和 Kibana 镜像 docker ps docker images从远程镜像仓库拉…

HarmonyOS后台代理提醒

后台代理提醒 简介 随着生活节奏的加快&#xff0c;我们有时会忘记一些重要的事情或日子&#xff0c;所以提醒功能必不可少。应用可能需要在指定的时刻&#xff0c;向用户发送一些业务提醒通知。例如购物类应用&#xff0c;希望在指定时间点提醒用户有优惠活动。为满足此类业…

redis:四、双写一致性的原理和解决方案(延时双删、分布式锁、异步通知MQ/canal)、面试回答模板

双写一致性 场景导入 如果现在有个数据要更新&#xff0c;是先删除缓存&#xff0c;还是先操作数据库呢&#xff1f;当多个线程同时进行访问数据的操作&#xff0c;又是什么情况呢&#xff1f; 以先删除缓存&#xff0c;再操作数据库为例 多个线程运行的正常的流程应该如下…

云原生之深入解析Kubernetes Operator的最佳实践和最常见的问题分析

一、Kubernetes Operator 简介 Kubernetes Operator 是通过连接主 API 并 watch 时间的一组进程&#xff0c;一般会 watch 有限的资源类型。当相关 watch 的 event 触发的时候&#xff0c;operator 做出响应并执行具体的动作。这可能仅限于与主 API 交互&#xff0c;但通常会涉…

HiveSql语法优化二 :join算法

Hive拥有多种join算法&#xff0c;包括Common Join&#xff0c;Map Join&#xff0c;Bucket Map Join&#xff0c;Sort Merge Buckt Map Join等&#xff0c;下面对每种join算法做简要说明&#xff1a; Common Join Common Join是Hive中最稳定的join算法&#xff0c;其通过一个M…

selenium+xpath爬取二手房标题

贝壳找房标题爬取需要注意的是&#xff0c;在页面中间有一个小广告 而他就在ul的li下面&#xff0c;当我们进行title所以输出时&#xff0c;会报错。 所以在进行页面解析之前必须把广告叉掉&#xff0c;不然也把广告那一部分的li给爬取下来了 所以&#xff0c;我们&#xff0…

听GPT 讲Rust源代码--src/tools(13)

File: rust/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/incoherent_impl.rs 在Rust源代码中&#xff0c;路径为rust/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/incoherent_impl.rs的文件是为了处理Rust代码中的不一致实现问题而存在的。…