整数集合

集合键

集合是指不重复且无序的字符串元素构成的整体(与列表不同的就是,它是无序且不重复的,所以不可以通过索引(列表有LIndex)来获取里面的值,只能逐个获取)。无序意味着里面所有元素的读写是可以任意的,不存在像列表一样一定是往两头插入或者在指定元素之间插入,或者是从两头开始弹出取值、通过索来取值。

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

整数集合的实现

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

每个intset.h/intset结构表示一个整数集合(是整个set集合)

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

}intset;
  1. contents数组是整数集合的底层实现,存储的就是集合里面的元素,而且是按从小到大有序地排列,并且数组中是不包含任何重复项的。
  2. length属性记录了整数集合包含的元素数量,即contents数组的长度

注意

contents属性声明为int8_t(8位),但实际上contents数组可能并不保存任何int8_t类型的值,contents数组的真正类型取决于encoding属性的值。

  • 如果encoding属性的值为INTSET_ENC_INT16,那么contents就是一个int16_t类型的数组,里面的元素都是一个16位数组(redis储存list redis储存 取出整数问题_数据
  • 同理,如果为INTSET_ENC_INT32和INTSET_ENC_INT64,就对应为32位和64位
  • 整个数组的字节大小,要通过length属性和encoding属性来计算,比如length值为5,encoding值为int16_t,那么总的字节大小为5*16=80。

下面看一种特殊情况

redis储存list redis储存 取出整数问题_数组_02


这是一个encoding属性为INTSET_ENC_INT64的,所以数组是储存64位的元素的数组,但里面除了第一个元素外,其他都不需要这么大的位数,不过根据整数集合的升级规则,当向一个底层位int16_t数组的整数集合中添加一个int64_t类型的整数值时,整数集合已有的所有元素都会被转换成int64_t类型。

升级

当我们要将一个新元素添加到底层数组时,会先比较新元素的位数是否满足,如果新元素的类型比整数集合现有所有的元素都要长时,整数集合需要先进行升级,然后才能将新元素添加到整数集合里面。

升级步骤总共有3个步骤

  • 根据新元素的类型,将整数集合的底层数组进行扩展,并为新元素分配空间
  • 将整数集合的底层数组里面的所有元素类型都变为新类型,并将类型转换后的元素放置到正确的位上(要按从小到大的顺序),而且在放置过程中,要维持有序性
  • 将新元素添加到底层数组里(也要维持有序性),一般这个放在最后,因为新元素是高位,所以都会比原有的低位元素要大

举个栗子来表示一下

redis储存list redis储存 取出整数问题_数组_03


比如这个集合里面有3个元素,是一个int16_t的数组,现在要添加一个redis储存list redis储存 取出整数问题_数据_04的数据,因为这个是一个31位的数据,所以要对数据进行升级首先第一步,根据新类型,对底层数组进行扩展,并为新元素分配空间,即为数组进行空间重分配,这里插入的是31位数据,然后分配后的空间为redis储存list redis储存 取出整数问题_数组_05,重新分配后的数组如下

redis储存list redis储存 取出整数问题_数据_06

此时,改变旧元素的类型,变成32位,旧元素有3个,即是32*3=96位(此时不管后面的新分配空间),此时一个索引位置所占的空间大小也要从16变成32位,然后进行对旧数据转移,比如旧数据3排第三位,应该放在索引2的位置上,也就是64~95位处

redis储存list redis储存 取出整数问题_数组_07


同理旧元素2的索引位置为1,在32位~63位的空间内,旧元素1的索引位置为0,在0 ~ 31位上完成这些后,才会对新插入的数据进行空间分配,分配到96~127位处

redis储存list redis储存 取出整数问题_底层实现_08


最后,程序将整数集合的encoding属性从INTSET_ENC_INT16改为INTSET_ENC_LNT32,并将length属性的值从3改为4,至此,升级完成。

关于时间复杂度

是要遍历整数集合将元素进行升级,所以向集合中添加元素的时间复杂度为redis储存list redis储存 取出整数问题_数组_09

关于元素摆放位置

引发升级的元素都是比所有元素长度大的,因为高位,所以要就大于所有元素,要么小于所有元素(针对插入的是负数情况),所以就分为下面两种情况进行

  • 在新元素小于所有元素的情况下,新元素会被放置在底层数组的最开头处(索引0)
  • 在新元素大于所有现有元素的情况下,新元素会被放置在底层数组的最末尾处,索引值为length-1

升级的好处

升级的策略有两个好处,一个是提升整数集合的灵活性,另一个是尽可能地节约内存。

提升灵活性

整数集合可以通过自动升级底层数组来适应元素,可以随意地添加16位、32位、64位的元素,避免了类型错误。

节约内存

要让一个数组可以同时存放16位、32位、64位的元素,最简单的做法就是直接使用64位的数组,即int64_t的数组,不过这样的话,假如添加的时候,没有64位元素,就会造成内存浪费。

而整数集合现在的做法既可以同时保存3种不同类型的值,又可以确保升级操作只在有需要的时间去进行,尽可能的节省下了内存

降级

整数集合是不支持降级操作的,一旦对数组升级了,编码即encode属性就会一直保持升级后的状态,假如一个数组有3个32位的,一个16位的,如果把32位的全删除了,只剩下16位了,这个数组依然是32位,不会降为16位。

重点

  • 整数集合是集合键的底层实现之一,针对里面的元素全是整形的集合
  • 整数集合的底层是数组,这个数组以有序、无重复的方式保存集合元素
  • 整数集合会根据新添加元素的类型,进行升级
  • 升级操作为整数集合提升了灵活性,又节约了内存
  • 整数集合只支持升级操作,不支持降级操作。