写在前面
以下内容是基于Redis 6.2.6 版本整理总结
一、整数集合(intset)
当一个集合只包含整数值元素,并且元素的个数不多时,Redis会使用整数集合作为集合键的底层实现。
1.1 整数集合的实现
整数集合可用保存的数据类型有:int16_t int32_t 和 int64_t 的整数值,并且保证集合中不会出现重复元素。
整数集合定义如下:
// src/intset.h typedef struct intset { uint32_t encoding; // 编码方式,后面会详细解释 uint32_t length; // 集合中元素的个数,也就是contents数组的长度 int8_t contents[]; // 保存元素的数组 } intset; /* Note that these encodings are ordered, so: * INTSET_ENC_INT16 < INTSET_ENC_INT32 < INTSET_ENC_INT64. */ #define INTSET_ENC_INT16 (sizeof(int16_t)) #define INTSET_ENC_INT32 (sizeof(int32_t)) #define INTSET_ENC_INT64 (sizeof(int64_t))
分析:
contents数组是整数集合的底层实现:整数集合中的每一个元素就是contents数组中的一个元素,每个元素在数组中按照从小到大的顺序排列,并且没有重复元素。
虽然,contents数组被声明为 int8_t 类型的数组,但实际上contents数组并不保存任何int8_t 类型的值,contents数组实际存储的类型取决于encoding的值。encoding的取值可以是:INTSET_ENC_INT16、INTSET_ENC_INT32 或 INTSET_ENC_INT64,每种编码的取值范围如下:
- INTSET_ENC_INT16 取值范围:[−2^15 ,2^15−1] 即:[-32768, 32767 ]
- INTSET_ENC_INT32 取值范围:[−2^31 ,2^31−1] 即:[-2147483648, 2147483647]
- INTSET_ENC_INT64 取值范围:[−2^63 ,2^63−1] 即:[-9223372036854775808, 9223372036854775807]
说明:n比特有符号整数的表示范围为:[−2^(n−1) ,2^(n−1)−1]
整数集合图示
1.2 整数集合的操作
每次往整数集合中插入新元素时,会检查新元素的类型,是不是比当前contents数组的类型长,如果是,整数集合需要先进行升级操作。比如:现在我们要把编码为int32_t的元素插入到整数集合里,因为65535的类型比集合中当前元素类型要长,所以要先对整数集合升级。
升级的好处:
- 灵活:C语言是静态类型的语言,为了避免类型错误,我们通常是一种类型就放到一种类型的数据结构里,如:我们一般会使用int16_t类型的数组保存int16_t类型的值,用int32_t类型数组保存int32_t类型的值。而整数集合允许随意的将不同类型的整型值添加到集合中,而不用担心类型错误。
- 节约内存:如果想让一个数组同时保存,int16_t、int32_t和int64_t的值,最简单的方式就是使用int64_t类型的数组,但是这会造成空间的浪费。inset只会在适当的时候才会触发升级操作,其他时间都是尽可能节约内存。
1.2.1 intset添加元素
inset插入元素
/* Insert an integer in the intset */ intset *intsetAdd(intset *is, int64_t value, uint8_t *success) { // 获取待插入整数的编码类型 uint8_t valenc = _intsetValueEncoding(value); uint32_t pos; if (success) *success = 1; /* Upgrade encoding if necessary. If we need to upgrade, we know that * this value should be either appended (if > 0) or prepended (if < 0), * because it lies outside the range of existing values. */ // 待插入整数的编码类型大于当前contents数组的编码类型,需要先进行升级操作 if (valenc > intrev32ifbe(is->encoding)) { /* This always succeeds, so we don't need to curry *success. */ return intsetUpgradeAndAdd(is,value); } else { /* Abort if the value is already present in the set. * This call will populate "pos" with the right position to insert * the value when it cannot be found. */ // 插入之前先在集合里查找是否已经存在该元素,(二分查找) // 如果该元素已经存在,则直接返回;如果没有,则返回该元素待插入的数组下标,保存在pos中 if (intsetSearch(is,value,&pos)) { if (success) *success = 0; return is; } // 1 3 5 insert 2 pos: 1 // 扩展空间 is = intsetResize(is,intrev32ifbe(is->length)+1); // 如果pos 小于当前数组长度,将从pos位置开始的元素,同一往后挪一格,腾出pos的位置 if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1); } // 将待插入的元素,放在pos的位置 _intsetSet(is,pos,value); // 更新数组的长度 is->length = intrev32ifbe(intrev32ifbe(is->length)+1); return is; } // 整数集合升级 static intset *intsetUpgradeAndAdd(intset *is, int64_t value) { // 获取当前整数集合的编码 uint8_t curenc = intrev32ifbe(is->encoding); // 计算待插入元素value的编码 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; } // 二分查找插入位置 static uint8_t intsetSearch(intset *is, int64_t value, uint32_t *pos) { int min = 0, max = intrev32ifbe(is->length)-1, mid = -1; int64_t cur = -1; /* The value can never be found when the set is empty */ if (intrev32ifbe(is->length) == 0) { if (pos) *pos = 0; return 0; } else { /* Check for the case where we know we cannot find the value, * but do know the insert position. */ if (value > _intsetGet(is,max)) { if (pos) *pos = intrev32ifbe(is->length); return 0; } else if (value < _intsetGet(is,0)) { if (pos) *pos = 0; return 0; } } while(max >= min) { mid = ((unsigned int)min + (unsigned int)max) >> 1; cur = _intsetGet(is,mid); if (value > cur) { min = mid+1; } else if (value < cur) { max = mid-1; } else { break; } } if (value == cur) { if (pos) *pos = mid; return 1; } else { if (pos) *pos = min; return 0; } } // 1 3 5 length:3 insert: 2 pos: 1 static void intsetMoveTail(intset *is, uint32_t from, uint32_t to) { void *src, *dst; uint32_t bytes = intrev32ifbe(is->length)-from; uint32_t encoding = intrev32ifbe(is->encoding); if (encoding == INTSET_ENC_INT64) { src = (int64_t*)is->contents+from; dst = (int64_t*)is->contents+to; bytes *= sizeof(int64_t); } else if (encoding == INTSET_ENC_INT32) { src = (int32_t*)is->contents+from; dst = (int32_t*)is->contents+to; bytes *= sizeof(int32_t); } else { src = (int16_t*)is->contents+from; dst = (int16_t*)is->contents+to; bytes *= sizeof(int16_t); } memmove(dst,src,bytes); }
插入元素步骤图解:
1.2.2 inset 删除元素:
/* Delete integer from intset */ intset *intsetRemove(intset *is, int64_t value, int *success) { // 获取待删除元素的编码 uint8_t valenc = _intsetValueEncoding(value); uint32_t pos; if (success) *success = 0; // 如果找到待删除元素及其pos if (valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,&pos)) { uint32_t len = intrev32ifbe(is->length); /* We know we can delete */ if (success) *success = 1; /* Overwrite value with tail and update length */ // 把从待删除元素的后面所有节点,往前挪动一格 if (pos < (len-1)) intsetMoveTail(is,pos+1,pos); is = intsetResize(is,len-1); is->length = intrev32ifbe(len-1); } return is; }
二、总结
- 整数集合的底层实现是数组,这个数组以有序、无重复的方式存储元素,在需要时会根据新添加元素的类型升级数组的类型。
- 只支持升级操作,不支持降级操作
- 整数集合是有序集合的底层实现之一