本月将开始分析redis源码系列:
同系列文章链接后期更新~~
1.Redis集合类型的元素都是整数并且都处在64位有符号整数范围之内时,并且元素个数不超过512个的时候,使用该结构体insert存储。
整数集合(intset)是Redis用于保存整数值的集合抽象数据结构,它可以保存类型为 int16_t、int32_t或者int64_t的整数值,并且保证集合中不会出现重复元素。
它在内存分配上与ziplist有些类似,是连续的一整块内存空间,而且对于大整数和小整数(按绝对值)采取了不同的编码,尽量对内存的使用进行了优化
127.0.0.1:6379> sadd testSet 1 2 3,
(integer) 4
127.0.0.1:6379> object encoding testSet
"intset"
上面的3个元素,其中存放的每个元素都占用16位,整数集合底层的大小为3*16 = 48位
当添加非整型变量,底层编码会发生转换
127.0.0.1:6379> sadd testSet 'a'
(integer) 1
127.0.0.1:6379> object encoding testSet
"hashtable"
2.insert数据结构
typedef struct intset {
uint32_t encoding;//编码类型
uint32_t length;//元素个数
int8_t contents[];//柔性数组,根据encoding字段决定几个字节表示一个元素,各个项在数组中按值得大小到大有序地排序,并且数组中不包含任何重复项
} intset
- length: 表示intset中的元素个数。encoding和length两个字段构成了intset的头部(header)。
- contents: 是一个柔性数组(flexible array member)表示intset的header后面紧跟着数据元素。这个数组的总长度(即总字节数)等于encoding * length。柔性数组在Redis的很多数据结构的定义中都出现过(例如sds, quicklist, skiplist),用于表达一个偏移量。contents需要单独为其分配空间,这部分内存不包含在intset结构当中。
3.元素的查找
uint8_t intsetFind(intset *is, int64_t value) {//第1个参数为待查询的Intset,第2个参数为待查找的值
uint8_t valenc = _intsetValueEncoding(value);//判断要插入valye的编码方式
//编码方式如果大于当前intset的编码方式,直接返回0。否则调用intsetSearch函数进行查找
return valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,NULL);
}
static uint8_t intsetSearch(intset *is, int64_t value, uint32_t *pos) {//可以看到该函数标签只比intsetFind多出第3个参数pos
//添加元素时会使用该函数进行查找,查找时会使用到pos这个参数,如果未查找到该元素,pos参数会记录需要插入该元素的位置。所以intsetFind函数调用intsetSearch时会将pos参数置为NULL
int min = 0, max = intrev32ifbe(is->length)-1, mid = -1;
int64_t cur = -1;
if (intrev32ifbe(is->length) == 0) { //如果intset中没有元素,直接返回0
if (pos) *pos = 0;
return 0;
} else { //如果元素大于最大值或者小于最小值,直接返回0
if (value > _intsetGet(is,intrev32ifbe(is->length)-1)) {
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) {//查找到返回1,未查找到返回0
if (pos) *pos = mid;
return 1;
} else {
if (pos) *pos = min;
return 0;
}
}
4.元素的插入
intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
uint8_t valenc = _intsetValueEncoding(value);//获取添加元素的编码值
uint32_t pos;
if (success) *success = 1;
if (valenc > intrev32ifbe(is->encoding)) {//如果大于当前intset的编码,说明需要进行升级
return intsetUpgradeAndAdd(is,value); //调用intsetUpgradeAndAdd进行升级后添加
} else {
if (intsetSearch(is,value,&pos)) {//否则先进行查重,如果已经存在该元素,直接返回,因为集合中不能存在重复的值
//传递了第3个参数,则插入成功时将第3个参数success的值置为1,如果该元素已经在集合中存在,则将success置为0
if (success) *success = 0;
return is;
}
//如果元素不存在,则添加元素
is = intsetResize(is,intrev32ifbe(is->length)+1);//首先将intset占用内存扩容
//如果插入元素的位置小于insert数据的末尾元素的位置,表示不是尾元素,调用intsetMoveTail给元素挪出空间
if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
}
_intsetSet(is,pos,value);//保存元素
is->length = intrev32ifbe(intrev32ifbe(is->length)+1);//修改intset的长度,将其加1
return is;
}
//虽然contents数组保存的四个整数值中,只有-2675256175807981027 是真正需要用int64_t类型来保存的,而其他 1、3、5三个值都可以用int16_t类型来保存,不过根据整数集合的升级规则,
//当向一个底层int16_t数组的整数集合添加一个int64_t类型的整数值时,整数集合已有的所有元素都会被转换成int64_t类型,所以contents数组保存的四个整数值都是int64_t类型的,
//升级编码格式后插入,返回值:添加新元素之后的集合
static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
uint8_t curenc = intrev32ifbe(is->encoding);//获取当前的编码值
uint8_t newenc = _intsetValueEncoding(value);//获取value需要的编码值
int length = intrev32ifbe(is->length);//获取元素个数
//因为value的编码比集合原有的所有的其他元素的编码都要大,所以,value要么大于集合中的所有元素,要么小于集合中的所有元素,因此,value只能添加到底层数组的最前端或最后端
//如果待插入元素小于0,说明需要插入到intset的头部位置。如果大于0,需要插到intset的末尾位置。
int prepend = value < 0 ? 1 : 0;
//更新集合编码方式
is->encoding = intrev32ifbe(newenc);
is = intsetResize(is,intrev32ifbe(is->length)+1);//将intset内存空间进行扩容
while(length--)
//从最后一个元素逐个往前扩容。将原来存储的数据从后往前重新设置。注意必须从最后一个元素开始,否则有可能会导致元素覆盖
//举个例子,假设原有的curenc编码的三个元素,它们在数组中排列如下:
//| x | y | z |
//当程序对数组进行重分配之后,数组就被扩容了(符号?表示未使用的内容):
//| x | y | z | ? | ? | ? |
//这时程序从数组的后端开始,重新插入元素,预留一个最后的位置给new
//| x | y | z | ? | z | ? |
//| x | y | y | z | ? |
//| x | y | z | ? |
//最后,程序可以将新元素添加到最后?号标示的位置中
//| x | y | z | new |
//上面演示的是新元素比原来的所有元素都大时的情况,也即是prepend == 0
//当新元素比原来的所有的元素都小时(prepend == 1),调整过程如下:
//| x | y | z | ? | ? | ? |
//| x | y | z | ? | ? | z |
//| x | y | z | ? | y | z |
//| x | y | x | y | z |
//当添加新值时,原本的| x | y |的数据将被新值代替
//| new | x | y | z |
_intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));
if (prepend)//如果待插入元素小于0,插入intset头部位置
_intsetSet(is,0,value);
else //否则插入末尾位置
_intsetSet(is,intrev32ifbe(is->length),value);
is->length = intrev32ifbe(intrev32ifbe(is->length)+1);//修改intset的长度,将其加1
return is;
}
5.元素的删除
//从整数集合中删除值value,success的值指示删除是否成功:因值不存在时造成删除失败,返回0 删除成功返回1
intset *intsetRemove(intset *is, int64_t value, int *success) {
uint8_t valenc = _intsetValueEncoding(value);//获取待删除元素编码
uint32_t pos;
if (success) *success = 0;//表示删除是否成功,这里表示默认删除失败
//待删除元素编码必须小于等于intset编码并且查找到该元素,才会执行删除操作
if (valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,&pos)) {
//使用len记录当前的元素个数
uint32_t len = intrev32ifbe(is->length);
if (success) *success = 1;
//上面的这句话if (valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,&pos)) ,调用的intsetSearch函数会更新pos值,找到要删除元素所在的位置
//如果待删除元素不是末尾元素,则调用intsetMoveTail直接覆盖掉该元素(则通过将position+1和之后位置的数据移动到position来覆盖掉position位置的值)
if (pos < (len-1)) {
intsetMoveTail(is,pos+1,pos);
} else {
//如果要删除的数据是该intset的最后一个值,假设该intset长度为length,则调用intsetResize分配length-1长度的空间之后会自动丢弃掉position位置的值
is = intsetResize(is,len-1);
}
is->length = intrev32ifbe(len-1);//修改intset的长度,将其减1
}
return is;
}