整数集合是 Redis 集合键的底层实现之一。当一个集合只包含整数值元素,并且元素数量不多时,Redis 就会使用整数集合作为集合键的底层实现。

1 整数集合的实现

整数集合是 Redis 用于保存整数值的集合抽象数据结构。它可以保存类型为 int16_t、int32_t、int64_t 的整数值,并且保证集合中不会出现重复元素。

每个 intset.h/intset 结构表示一个整数集合:

typedef struct intset {
uint32_t encoding;
uint32_t length;
int8_t contents[];
} intset;
  • encding:编码方式
  • length:集合包含的元素数量
  • contents[]:保存元素的数组

contents 数组是整数集合的底层实现:整数集合的每个元素都是 contents 数组的一个数组项,各个项在数组中按值的大小从小到大有序排列,并且数组中不包含重复项。

length 属性记录了整数集合记录的元素数量,也就是 contents 数组的长度。

虽然 intset 结构将 contents 属性声明为 int8_t 类型的数组,但实际上 contents 数组并不保存任何 int8_t 类型的值,contents 数组的真正类型取决于 encoding 属性的值,比如:如果 encoding 属性的值为 INTSET_ENC_INT16,那么 contents 就是一个 int16_t 类型的数组,数组里的每个项都是一个 int16_t 类型的整数值,取值范围为:[-32768-32767](2^(16-1))。

与之类似,encoding 的值为 INTSET_ENC_INT32,那么数组每项的取值范围为:[-2147483648, 2147483647](2^(32-1)。

这里也引发了一个问题,当我们对一个 encoding 为 INTSET_ENC_INT8 的 intset,插入 129 时(int8_t 的取值范围是 [-128, 127]),会出现什么?

这也就引发了 intset 的升级操作。与之对应,也有降级操作。接下来,我们来详细认识下 intset 的升降级操作。

2 升级操作

每当我们要将一个新元素添加到整数集合时,如果新元素的类型比整数集合的 encoding 类型大,整数集合就需要先进行升级操作(upgrade),然后才能将新元素添加到整数集合中。

整个升级操作源码如下:

// intset.c/intsetUpgradeAndAdd()
/* Upgrades the intset to a larger encoding and inserts the given integer. */
static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
uint8_t curenc = intrev32ifbe(is->encoding);
uint8_t newenc = _intsetValueEncoding(value);
int length = intrev32ifbe(is->length);
int prepend = value < 0 ? 1 : 0; /* First set new encoding and resize */
is->encoding = intrev32ifbe(newenc);
is = intsetResize(is,intrev32ifbe(is->length)+1); /* Upgrade back-to-front so we don't overwrite values.
* Note that the "prepend" variable is used to make sure we have an empty
* space at either the beginning or the end of the intset. */
while(length--)
_intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc)); /* Set the value at the beginning or the end. */
if (prepend)
_intsetSet(is,0,value);
else
_intsetSet(is,intrev32ifbe(is->length),value);
is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
return is;
}

升级整数集合并添加新元素,共分为三步进行:

  1. 扩展底层数组大小。根据新元素的类型,扩展整数集合底层数组的大小,并为新元素分配空间。
  2. 元素转换,并保持原有顺序。将底层数组现有的所有元素,都转换成与新元素相同的类型,并将转换后的元素放在正确的位置上,保证原有顺序不发生改变。
  3. 将新元素添加到底层数组中。

此外,一旦因插入新元素引发升级操作,就说明新插入的元素比集合中现有的所有元素的长度大,所以这个新元素的值要么大于所有现有元素(正值),要么就小于所有现有元素(负值),那么:

  • 在新元素小于所有现有元素时,新元素就会被放在底层数组的最开头的位置,即索引为 0 的位置;
  • 在新元素大于所有现有元素时,新元素就会被放在底层数组的最末尾的位置;

3 升级优势

整数集合的升级策略主要有以下两个好处:

  1. 提示整数集合的灵活性;
  2. 尽可能的节约内存;

3.1 提示灵活性

因为 C 语言是静态类型语言,为了避免类型错误,我们通常不会将两种不同类型的值放在同一个数据结构中。

但是,因为有了升级操作,整数集合可以通过它来自适应新元素,所以我们可以随意地将 int16_t、int32_t、和 int64_t 类型的整数添加到集合中,而不必担心出现类型错误,大大的提升了整数集合的灵活性。

3.2 节约内存

当然,要让一个数组可以同时保存 int16_t、int32_t、和 int64_t 类型的整数值,我们可以粗暴的直接使用 int64_t 类型的数组作为整数集合的底层实现,来保存不同类型的值。但是,这样一来,即使添加到集合中的都是 int16_t、int32_t 类型的值,数组也都是需要使用 int64_t 类型的空间去保存,出现浪费内存的情况。

而整数集合的升级操作,既能同时保存三种不同类型的值,又可以确保升级操作只会在有需要的时候进行,达到节省内存的目的。

4 交、并、差集算法

Redis 中的集合实现了交、并、差等操作,相关操作可参加 t_set.c,其中

sinterGenericCommand() 实现交集,sunionDiffGenericCommand() 实现并集和差集。

它们都能同时对多个集合进行元素。当对多个集合进行差集运算时,会先计算出第一个和第二个集合的差值,然后再与第三个集合做差集,依次类推。

接下来,我们一起来认识下三个操作的实现思路。

4.1 交集

计算交集的过程大概可以分为三部分:

  1. 检查各个集合,对于不存在的集合当做空集来处理。一旦出现空集,则不用继续计算了,最终的交集就是空集。
  2. 对各个集合按照元素个数由少到多进行排序。这个排序有利于后面计算的时候从最小的集合开始,需要处理的元素个数较少。
  3. 对排序后第一个集合(也就是最小集合)进行遍历,对于它的每一个元素,依次在后面的所有集合中进行查找。只有在所有集合中都能找到的元素,才加入到最后的结果集合中。

需要注意的是,上述第 3 步在集合中进行查找,对于 intset 和 dict 的存储来说时间复杂度分别是 O(log n) 和 O(1)。但由于只有小集合才使用 intset,所以可以粗略地认为 intset 的查找也是常数时间复杂度的。

4.2 并集

并集操作最简单,只要遍历所有集合,将每一个元素都添加到最后的结果集中即可。向集合中添加元素会自动去重,所以插入的时候无需检测元素是否已存在。

4.3 差集

计算差集有两种可能的算法,它们的时间复杂度有所区别。

第一种算法

对第一个集合进行遍历,对于它的每一个元素,依次在后面的所有集合中进行查找。只有在所有集合中都找不到的元素,才加入到最后的结果集合中。

这种算法的时间复杂度为O(N*M),其中N是第一个集合的元素个数,M是集合数目。

第二种算法

  1. 将第一个集合的所有元素都加入到一个中间集合中。
  2. 遍历后面所有的集合,对于碰到的每一个元素,从中间集合中删掉它。
  3. 最后中间集合剩下的元素就构成了差集。
  4. 这种算法的时间复杂度为O(N),其中N是所有集合的元素个数总和。

在计算差集的开始部分,会先分别估算一下两种算法预期的时间复杂度,然后选择复杂度低的算法来进行运算。还有两点需要注意:

  • 在一定程度上优先选择第一种算法,因为它涉及到的操作比较少,只用添加,而第二种算法要先添加再删除。
  • 如果选择了第一种算法,那么在执行该算法之前,Redis的实现中对于第二个集合之后的所有集合,按照元素个数由多到少进行了排序。这个排序有利于以更大的概率查找到元素,从而更快地结束查找。

5 总结

  1. 整数集合是集合键的底层实现之一。
  2. 整数集合以有序、无重复的方式保存集合元素。在有需要时,会根据新添加元素的类型,改变底层数组的类型。
  3. 升级操作提升了操作的灵活性,并尽可能的节约了内存。
  4. 集合可以进行交、并、差集操作。

跟着大彬读源码 - Redis 10 - 对象编码之整数集合的更多相关文章

  1. 跟着大彬读源码 - Redis 7 - 对象编码之简单动态字符串

    Redis 没有直接使用 C 语言传统的字符串表示(以空字符串结尾的字符数组),而是构建了一种名为简单动态字符串(simple dynamic string)的抽象类型,并将 SDS 用作 Redis ...

  2. 跟着大彬读源码 - Redis 8 - 对象编码之字典

    目录 1 字典的实现 2 插入算法 3 rehash 与 渐进式 rehash 总结 字典,是一种用于保存键值对的抽象数据结构.由于 C 语言没有内置字典这种数据结构,因此 Redis 构建了自己的字 ...

  3. 跟着大彬读源码 - Redis 9 - 对象编码之 三种list

    目录 1 ziplist 2 skiplist 3 quicklist 总结 Redis 底层使用了 ziplist.skiplist 和 quicklist 三种 list 结构来实现相关对象.顾名 ...

  4. 跟着大彬读源码 - Redis 6 - 对象和数据类型(下)

    继续撸我们的对象和数据类型. 上节我们一起认识了字符串和列表,接下来还有哈希.集合和有序集合. 1 哈希对象 哈希对象的可选编码分别是:ziplist 和 hashtable. 1.1 ziplist ...

  5. 跟着大彬读源码 - Redis 5 - 对象和数据类型(上)

    相信很多人应该都知道 Redis 有五种数据类型:字符串.列表.哈希.集合和有序集合.但这五种数据类型是什么含义?Redis 的数据又是怎样存储的?今天我们一起来认识下 Redis 这五种数据结构的含 ...

  6. 跟着大彬读源码 - Redis 1 - 启动服务,程序都干了什么?

    一直很羡慕那些能读 Redis 源码的童鞋,也一直想自己解读一遍,但迫于 C 大魔王的压力,解读日期遥遥无期. 相信很多小伙伴应该也都对或曾对源码感兴趣,但一来觉得自己不会 C 语言,二来也不知从何入 ...

  7. 跟着大彬读源码 - Redis 3 - 服务器如何响应客户端请求?(下)

    继续我们上一节的讨论.服务器启动了,客户端也发送命令了.接下来,就要到服务器"表演"的时刻了. 1 服务器处理 服务器读取到命令请求后,会进行一系列的处理. 1.1 读取命令请求 ...

  8. 跟着大彬读源码 - Redis 4 - 服务器的事件驱动有什么含义?(上)

    众所周知,Redis 服务器是一个事件驱动程序.那么事件驱动对于 Redis 而言有什么含义?源码中又是如何实现事件驱动的呢?今天,我们一起来认识下 Redis 服务器的事件驱动. 对于 Redis ...

  9. 跟着大彬读源码 - Redis 2 - 服务器如何响应客户端请求?(上)

    上次我们通过问题"启动服务器,程序都干了什么?",跟着源码,深入了解了 Redis 服务器的启动过程. 既然启动了 Redis 服务器,那我们就要连上 Redis 服务干些事情.这 ...

随机推荐

  1. flask高级编程 LocalStack 线程隔离

    转:https://www.cnblogs.com/wangmingtao/p/9372611.html   30.LocalStack作为线程隔离对象的意义 30.1 数据结构 限制了某些能力 30 ...

  2. Maxon Cinema 4D Studio R20.026 中文破解版下载

    Maxon Cinema 4D Studio,是 Maxon 公司开发的一款专业三维工具包,如果你需要一个得力助手,轻松快速创建令人称赞的 3D 图形作品,那么这是你的最佳选择. 为何使用Cinema ...

  3. python接口自动化(三十二)--Python发送邮件(常见四种邮件内容)番外篇——上(详解)

    简介 本篇文章与前边没有多大关联,就是对前边有关发邮件的总结和梳理.在写脚本时,放到后台运行,想知道执行情况,会通过邮件.SMS(短信).飞信.微信等方式通知管理员,用的最多的是邮件.在linux下, ...

  4. springmvc上传文件踩过的坑

    @RequestMapping("/addTweet") public String addTweet(TweetVO tweetVO, HttpServletRequest re ...

  5. CDQZ集训DAY0 日记

    貌似没发生什么事…… 按照教练员的交代,写一下流水账…… 早上5:30到了机场,然后就默默地坐着飞机到了成都.然后就按预定好的被GXY的父亲的朋友接机(貌似因为觉得GXY和他爸的同学挺像被批判一番). ...

  6. 这样子来理解C语言中指针的指针

    友情提示:阅读本文前,请先参考我的之前的文章<从四个属性的角度来理解C语言的指针也许会更好理解>,若已阅读,请继续往下看. 我从4个属性的角度来总结了C语言中的指针概念.对于C语言的一个指 ...

  7. Docker笔记(五):整一个自己的镜像

    原文地址:http://blog.jboost.cn/2019/07/17/docerk-5.html 获取镜像的途径有两个,一是从镜像仓库获取,如官方的Docker Hub,二是自定义.上文已经介绍 ...

  8. Thread API的详细介绍

    import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurr ...

  9. C#4.0新增功能04 嵌入的互操作类型

    连载目录    [已更新最新开发文章,点击查看详细] 从 .NET Framework 4 开始,公共语言运行时支持将 COM 类型的类型信息直接嵌入到托管程序集中,而不要求托管程序集从互操作程序集中 ...

  10. 微信小程序踩坑日记1——调用微信授权窗口

    0. 引言 微信小程序为了优化用户体验,取消了在进入小程序时立马出现授权窗口.需要用户主动点击按钮,触发授权窗口. 那么,在我实践过程中,出现了以下问题. . 无法弹出授权窗口 . 希望在用户已经授权 ...