0. 源码注释位置
1. 何为整数集合
整数集合,顾名思义,是整数的集合,用于实现redis中的set,redis中的set也可以通过哈希表实现,为什么要有整数集合呢?这里考虑的是省内存,整数集合在数据量较小的情况下会占用更少的内存。
内存映射数据结构是一系列经过特殊编码的字节序列,创建它们所消耗的内存通常比作用类似
的内部数据结构要少得多,如果使用得当,内存映射数据结构可以为用户节省大量的内存。
不过,因为内存映射数据结构的编码和操作方式要比内部数据结构要复杂得多,所以内存映射
数据结构所占用的CPU 时间会比作用类似的内部数据结构要多。——《redis设计与实现》
整数集合会根据元素的大小自动选择合适的数据类型存储元素,如元素最大不会超过
2
16
2^{16}
216,则数据类型就选择int16_t
,如果在插入数据的过程中出现数据的大小大于
2
16
2^{16}
216,则使用能表示更大范围的数据类型,如int32_t
或int64_t
。这是自动调整的。
2. inset结构体与API
2.1 结构体
typedef struct intset {
uint32_t encoding; //集合中数据的编码方式
uint32_t length; //集合中元素的个数
int8_t contents[]; //存储集合的元素
} intset;
inset的结构体比较简单,使用一个柔性数组存储集合中的元素。
2.2 API
intset *intsetNew(void);//创建集合
intset *intsetAdd(intset *is, int64_t value, uint8_t *success);//将value加入集合中
intset *intsetRemove(intset *is, int64_t value, int *success);//将value从集合中删除
uint8_t intsetFind(intset *is, int64_t value);//在集合中查找value
int64_t intsetRandom(intset *is);//随机获取一个值
uint8_t intsetGet(intset *is, uint32_t pos, int64_t *value);//获取指定位置的值
uint32_t intsetLen(const intset *is);//获取集合长度
size_t intsetBlobLen(intset *is);//整个集合占用的字节数
不多且不难。
3. API详解
3.1 根据数据类型获取编码方式_intsetValueEncoding
static uint8_t _intsetValueEncoding(int64_t v) { //返回数据需要的编码类型
if (v < INT32_MIN || v > INT32_MAX) //32位表示不了则使用INTSET_ENC_INT64
return INTSET_ENC_INT64;
else if (v < INT16_MIN || v > INT16_MAX)//32位表示的了但是16位表示不了则使用INTSET_ENC_INT32
return INTSET_ENC_INT32;
else
return INTSET_ENC_INT16;//16位表示的了则使用INTSET_ENC_INT16
}
3.2 获取集合中的某个值
static int64_t _intsetGetEncoded(intset *is, int pos, uint8_t enc) { //获取集合is中位置为pos的数
int64_t v64;
int32_t v32;
int16_t v16;
if (enc == INTSET_ENC_INT64) {
memcpy(&v64,((int64_t*)is->contents)+pos,sizeof(v64));//根据对应的编码类型,找到pos位置的数并可拷贝
memrev64ifbe(&v64); //如果有需要的话,memrevEncifbe(&vEnc) 会对拷贝出的字节进行大小端转换
return v64; //返回值
} else if (enc == INTSET_ENC_INT32) {
memcpy(&v32,((int32_t*)is->contents)+pos,sizeof(v32));
memrev32ifbe(&v32);
return v32;
} else {
memcpy(&v16,((int16_t*)is->contents)+pos,sizeof(v16));
memrev16ifbe(&v16);
return v16;
}
}
/* Return the value at pos, using the configured encoding. */
static int64_t _intsetGet(intset *is, int pos) {
return _intsetGetEncoded(is,pos,intrev32ifbe(is->encoding));//返回pos位置上的数,intrev32ifbe函数是说,如果编码方式为大端,则将参数由小端模式转变为大端模式
}
3.3 设置集合中某位置的值_intsetSet
static void _intsetSet(intset *is, int pos, int64_t value) { //将位置为pos的数改变为value
uint32_t encoding = intrev32ifbe(is->encoding);//获得编码方式
if (encoding == INTSET_ENC_INT64) {
((int64_t*)is->contents)[pos] = value; //将pos位置的数设置为value
memrev64ifbe(((int64_t*)is->contents)+pos); //如果需要转为大端则将数转换为大端
} else if (encoding == INTSET_ENC_INT32) {
((int32_t*)is->contents)[pos] = value;
memrev32ifbe(((int32_t*)is->contents)+pos);
} else {
((int16_t*)is->contents)[pos] = value;
memrev16ifbe(((int16_t*)is->contents)+pos);
}
}
3.4 创建新的集合intsetNew
集合的默认编码方式为INTSET_ENC_INT16
intset *intsetNew(void) { //创建一个新的set
intset *is = zmalloc(sizeof(intset));//分配空间
is->encoding = intrev32ifbe(INTSET_ENC_INT16);//设置默认的编码模式为INTSET_ENC_INT16
is->length = 0;//长度设置为0
return is;
}
3.5 改变集合大小
static intset *intsetResize(intset *is, uint32_t len) { //重新改变inset的大小使得数据容量为len
uint32_t size = len*intrev32ifbe(is->encoding);//数据所需要的空间
is = zrealloc(is,sizeof(intset)+size);//数据的空间加上inset中encoding和length的空间
return is;
}
3.6 在集合中查找某个元素intsetSearch
static uint8_t intsetSearch(intset *is, int64_t value, uint32_t *pos)
如果找到,则返回1,并将pos指向的内容设置为value在集合中的位置;
如果找不到,则返回0,并将pos设置为应该插入的位置。
整个集合是有序的,
先判断集合是否为空,
再判断插入的值是不是比最大值大或者比最小值小,
最后使用二分查找找到要插入的位置。
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) { //集合长度为0,位置pos设置为0,返回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)) { //比最大值还大,插入位置在尾部,返回0表示找不到
if (pos) *pos = intrev32ifbe(is->length);
return 0;
} else if (value < _intsetGet(is,0)) { //比最小值还小,插入位置在头部,返回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;
}
}
3.7 更新编码方式并插入一个大值intsetUpgradeAndAdd
static intset *intsetUpgradeAndAdd(intset *is, int64_t value)
该函数将更新集合的编码方式,将其变大,并将value插入集合中,这里的value一定是原来集合编码方式所表示不了的,要么大于最大值,要么小于最小值,插入的位置只能是头部或者尾部。
static intset *intsetUpgradeAndAdd(intset *is, int64_t value) { //将集合的编码方式变大并插入value
uint8_t curenc = intrev32ifbe(is->encoding); //现在的编码方式
uint8_t newenc = _intsetValueEncoding(value); //新的编码方式
int length = intrev32ifbe(is->length); //当前的元素数目
/*由于编码方式变大,说明新插入的数由当前的编码方式表示不了,其要么大于最大值,要么小于最小值,
由于集合是有序的,这个数插入的位置只能是头部或尾部,
所以value小于0表示插入到头部,大于0表示插入到尾部*/
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--) //搬移原来集合的元素,如果value<0则所有元素后移一个位置,否则位置不变(但内存空间中会改变,逻辑位置不变)
_intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));
/* Set the value at the beginning or the end. */
if (prepend)
_intsetSet(is,0,value);//value小于0则将其插入头部
else
_intsetSet(is,intrev32ifbe(is->length),value);//否则插入尾部,比最大值还大
is->length = intrev32ifbe(intrev32ifbe(is->length)+1);//设置新的长度
return is;//返回新的集合
}
关于prepend,可以参考:黄健宏redis源码
3.8 插入一个值intsetAdd
intset *intsetAdd(intset *is, int64_t value, uint8_t *success)
value为新插入的值,*success表示插入成功与否,1表示插入成功,0表示插入失败。
插入的值可能会导致编码方式的改变;
插入的值可能已经存在集合中,此时插入失败;
插入的值位置可能在集合中间,此时需要将对应位置后面的元素向后移;
结合上面3点考虑再看代码就比较容易:
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. */
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. */
if (intsetSearch(is,value,&pos)) { //先查找元素,如果找到则不用插入,pos保存元素的位置或应该插入的位置
if (success) *success = 0;//添加失败,*success设置为0
return is;
}
is = intsetResize(is,intrev32ifbe(is->length)+1);//扩大集合大小
if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);//如果插入的位置不是末尾,则需要将原来的元素往后移动,空出一个位置
}
_intsetSet(is,pos,value);//设置对应位置的值
is->length = intrev32ifbe(intrev32ifbe(is->length)+1);//长度加一
return is;
}
3.9 删除一个值
intset *intsetRemove(intset *is, int64_t value, int *success)
从集合中删除value。*success为1表示成功,0表示失败。
删除的元素要在集合中;
删除的位置在末尾的话,直接缩小集合的大小即可;
删除的位置在中间的话,使用对应位置后面的元素覆盖前面的,往前移动。
intset *intsetRemove(intset *is, int64_t value, int *success) { //从集合中删除一个值
uint8_t valenc = _intsetValueEncoding(value);//value对应的编码模式
uint32_t pos;
if (success) *success = 0;//0表示删除失败
if (valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,&pos)) { //value的编码模式比集合的编码模式小且value在集合中才可以删除,否则不用删除
uint32_t len = intrev32ifbe(is->length);//集合长度
/* We know we can delete */
if (success) *success = 1;//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;
}
3.10 查找value是否在集合中intsetFind
uint8_t intsetFind(intset *is, int64_t value) { //在集合中查找值
uint8_t valenc = _intsetValueEncoding(value);//值对应的编码
return valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,NULL);//编码小于集合编码且在集合中
}
4. 总结
除了自适应的编码方式外,别的没有什么难理解的,可以简单的认为这就是一个有序数组。
算是和双端链表一样比较简单的了。