Redis源码阅读【1-简单动态字符串】
Redis源码阅读【2-跳跃表】
Redis源码阅读【3-Redis编译与GDB调试】
Redis源码阅读【4-压缩列表】
Redis源码阅读【5-字典】
Redis源码阅读【6-整数集合】
Redis源码阅读【7-quicklist】
Redis源码阅读【8-命令处理生命周期-1】
Redis源码阅读【8-命令处理生命周期-2】
Redis源码阅读【8-命令处理生命周期-3】
Redis源码阅读【8-命令处理生命周期-4】
Redis源码阅读【番外篇-Redis的多线程】
Redis源码阅读【9-持久化】
Redis源码阅读【10-事务与Lua】
建议搭配源码阅读:源码地址
1、介绍
整数集合(intset)是一个有序,存储整型数据的结构。我们知道Redis是一个内存数据库,所以必须考虑如何才能高效的利用内存,使用传统字符串的方式来存储整型,无疑是浪费内存空间,当Redis集合类型的元素都是整数并且都处在64位有符号整数范围之内时,使用该结构体存储。我们先执行一段代码如下所示:
当我们添加元素 1 2 -1 -6 的时候,底层的存储结构是 intset 但是当我, 再添加元素 ‘a’ 的时候底层元素变成了 hashtable
2、数据存储
整数集合在Redis中可以保存int16_t
,int32_t
,int64_t
类型的整型数据,并且可以保证集合中不会出现重复数据。每个整数集合使用一个intset类型的数据结构表示。intset结构如下:
typedef struct intset {
uint32_t encoding; //编码类型
uint32_t length;// 元素个数
int8_t contents[]; // 柔性数组,根据encoding字段决定几个字节表示一个元素
} intset;
encoding
:编码类型,决定每个元素占用几个字节。有如下三种类型:
INTSET_ENC_INT16
:当元素值都位于INT16_MIN 和 INT16_MAX 之间时使用。该编码方式为每个元素占用2
个字节
INTSET_ENC_INT32
:当元素值位于 INT16_MAX 到 INT32_MAX 或者INT32_MIN 到 INT16_MIN 之间使用。该编码方式为每个元素占用4
个字节。
INTSET_ENC_INT64
:当元素值位于INT32_MAX 到 INT64_MAX 或者 INT64_MIN 到 INT32_MIN 之间时使用。该编码方式为每个元素占用8
个字节
编码 | 值 |
---|---|
INTSET_ENC_INT16 | (2147483647,9223372036854775807)或[-9223372036854775808,-2147483648) |
INTSET_ENC_INT32 | (32767,2147483647) 或 [-2147483648,-32768) |
INTSET_ENC_INT64 | [-32768,32767] |
intset 结构体会根据待插入的值决定是否需要进行扩容操作。扩容会修改encoding字段,而encoding
字段决定了一个元素在contents柔性数组中占用几个字节。所以当修改encoding
字段之后,intset中原来的元素也需要在contents中进行相应的扩展。注意这里就有一个理论,只要待插入的值导致扩容,则该值在待插入的intset中不是最大值,就是最小值
encoding 字段在Redis中使用宏来表示。其定义如下:
//整数集合定义宏
#define INTSET_ENC_INT16 (sizeof(int16_t)) // int 16 2字节
#define INTSET_ENC_INT32 (sizeof(int32_t)) // int 32 4字节
#define INTSET_ENC_INT64 (sizeof(int64_t)) // int 64 8字节
因为encoding字段实际取值为 2、4、8,所以encoding字段可以直接比较大小。当待插入值的encoding 字段 大于待插入intset 的 encoding时,说明需要进行扩容操作,并且也能表明该待插入值在该intset中肯定不存在。
length
:元素个数。即一个intset中包括多少个元素。
contents
:存储具体元素。根据 encoding 字段决定多少个字节表示一个元素。
按此存储结构,上文示例中生成的testSet存储内容如图所示:
encoding
字段为2,代表INTSET_ENC_INT16。length 字段为 4,代表该intset中有4个元素。根据encoding字段,每个元素分别占用两个字节,并且按从小到大的顺序排列,依次为-6、-1、1、2
。
3、基本操作
下面将介绍一下intset中查询,添加,删除
元素操作,以及介绍一下其常用的API
3.1、查询元素
查询元素的入口函数是intsetFind,该函数首先进行一些防御性判断,如果没有通过判断则直接返回。intset是按从小到大有序排列的,所以通过防御性判断之后使用二分进行元素查找(因为是有序的)。一下是Redis中intset查询代码的实现:
//intset查询
uint8_t intsetFind(intset *is, int64_t value) {
uint8_t valenc = _intsetValueEncoding(value); //判断编码方式
return valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,NULL);
//编码方式如果大于当前intset的编码方式,直接返回0。否则调用intsetSearch函数进行查找
}
//intset具体查询
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;
//如果intset没有元素,直接返回0
if (intrev32ifbe(is->length) == 0) {
if (pos) *pos = 0;
return 0;
} else {
//如果元素大于最大值或者最小值,直接返回
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) {
//这里使用了位运算 同时使用了unsigned c语言和 java 不太一样 >> 1 代表除于二
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;
}
}
//判断元素是否找到找到返回1 未找到返回0
if (value == cur) {
if (pos) *pos = mid;
return 1;
} else {
if (pos) *pos = min;
return 0;
}
}
intsetSearch
就是查找的核心函数,其本质就是使用了二分查找法,具体流程如下:
1、函数定义
uint8_t intsetFind(intset *is,int64_t value)
。第一给参数为待查询的intset,第2个参数为待查找的值。首先判断待查找的值需要的编码格式(如上表中所示),如果编码大于该intset的编码,则值肯定不再其中,直接返回,否则调用intsetSearch
函数继续查找。2、
intsetFind
继续内部调用intsetSearch
,先判断intset中是否有值,如果当前intset为空,则直接返回0。如果有值,判断当前目标值是否介于intset的最大和最小值直接,如果否也直接返回0。
3、因为intset是一个有序数组,则直接使用 二分查找 来查询目标元素,找到返回1 否则返回0
3.2、添加元素
添加元素的入口函数是intsetAdd
,该函数根据插入值的编码类型和当前intset 的编码类型决定是直接插入还是进行 intset 升级扩展后再执行插入(升级插入的函数入口为intsetUpgradeAndAdd
)如下是代码实现:
//intset添加元素
intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
//获取目标元素的编码
uint8_t valenc = _intsetValueEncoding(value);
uint32_t pos;
if (success) *success = 1;
//如果大于当前intset的编码则,说明要进行升级
if (valenc > intrev32ifbe(is->encoding)) {
/* This always succeeds, so we don't need to curry *success. */
//调用升级函数
return intsetUpgradeAndAdd(is,value);
} else {
//否则先进行查重,如果已经存在该元素,直接返回
if (intsetSearch(is,value,&pos)) {
if (success) *success = 0;
return is;
}
//如果元素不存在,则添加元素
is = intsetResize(is,intrev32ifbe(is->length)+1);//首先将intset占用内存扩容
//如果插入元素再intset中间位置,调用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;
}
添加元素的流程如下:
1
、函数定义为intset * intsetAdd(intset *is,int64_t value,unint8_t *success);
其第1个参数为待添加元素的 intset,第2个参数 为待插入的值,第3个参数 选传,如果该元素已经在集合中存在,将success 置0
2
、判断要插入的值需要什么编码格式。如果当前intset 的编码格式小于待插入的值需要的编码格式,则调用intsetUpgradeAndAdd
进行升级(调用intsetUpgradeAndAdd会改变encoding)
3
、调用intsetSearch 函数进行查重,即插入的值是否在当前集合中,如果找到了就不能再次插入,直接返回。如果没有找到,在intsetSearch
中会将待插入值需要插入的位置赋值给position
。position 的计算逻辑也比较简单,首先如果 intset 为空,则需要将待插入值置于该 intset 的第一个位置,即 position 为0,如果待插入的值小于 intset 最小值,position 也为0 ,如果待插入值大于intset 的最大值,待插入值需要放到 intset 最后一个位置,即 position 为 intset 的长度,如果上述几种情况都不满足,position 为该 intset 中待插入值小于的第一个数之前的位置。
4
、调用intsetResize
扩充当前的intset,即给新插入的值申请好存储空间。假设原来的元素个数为length,编码方式为 encoding,则intsetResize
会重新分配一块内存,大小为:encoding * (length + 1)
。
5
、如果要插入的位置于原来元素之间,则调用intsetMoveTail
将 position 开始的数据移动到 position +1 的位置。
6
、插入新值并将 intset 的长度字段 length 加1。
其中intsetMoveTail
的操作如下图:
那么如果intset需要升级整个流程是怎么样的呢?
当
intsetAdd
函数判断当前编码类型不能存放需要添加的新值时候,会调用intsetUpgradeAndAdd
函数先升级当前的编码类型。并且按新编码类型重新存储现有数据,然后将新的元素添加进去。
代码如下:
static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
uint8_t curenc = intrev32ifbe(is->encoding); // 获取当前编码类型
uint8_t newenc = _intsetValueEncoding(value); // 确定编码类型
int length = intrev32ifbe(is->length);
//如果待插入元素小于0,说明需要插入到intset的头部位置。如果大于0,需要插到intset的末尾位置。
//因为既然需要扩容了,那么待插入的元素,不是最大值就是最小值
int prepend = value < 0 ? 1 : 0;
is->encoding = intrev32ifbe(newenc);
is = intsetResize(is,intrev32ifbe(is->length)+1);
//从最后一个元素逐个往前扩容。从最后一个开始,这样就不会出现覆盖的情况
while(length--)
_intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));
//如果待插入元素小于0,插入intset头部位置
if (prepend)
_intsetSet(is,0,value);
else //否则插入到尾部
_intsetSet(is,intrev32ifbe(is->length),value);
is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
return is;
}
Redis intset升级并添加元素的过程如下:
1、函数定义为
static intset *intsetUpgradeAndAdd(intset *is,int64_t value)
。
2、根据新的编码方式调用intsetResize
重新申请空间。假设新编码方式为encodingNew,现有元素个数为length,则需要的空间为encodingNew * (length +1)
。
3、移动并扩容原来的元素。注意扩容原来的元素时,按照从后往前的顺序依次扩容,这样可以避免数据被覆盖。如果该值是正数则该值是最大值放在最后一位,如果是负数,则该值是最小值,放在第一位。
4、根据新插入是正数还是负数,将值插入相应的位置。
5、插入新值并将intset的长度字段length加1。
到这里这个添加流程就结束了。
3.3、删除元素
intset删除元素的入口函数是intsetRemove
,该函数查找需要删除的元素然后通过内存地址的移动直接将该元素覆盖。代码如下:
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)) {
uint32_t len = intrev32ifbe(is->length);
if (success) *success = 1;
//如果待删除元素位于中间位置,则调用intsetMoveTail直接覆盖掉该元素
//如果待删除元素位于intset末尾,则intset收缩内存后直接将其丢弃
if (pos < (len-1)) intsetMoveTail(is,pos+1,pos);
is = intsetResize(is,len-1);
is->length = intrev32ifbe(len-1);//修改intset的长度,将其减1
}
return is;
}
删除流程整体如下:
1、函数定义为
intset *intsetRemove(initset *is,int64_t value,int *success)
,首先判断编码是否小于等于当前编码,若不是直接返回。
2、调用intsetSearch
查找该值是否存在,不存在直接返回,存在则获取该值所在位置 position。
3、如果要删除的数据不是该 intset 的最后一个值,则通过将 position+1 和之后位置的数据移动到 position 来覆盖掉 position 位置的值。(如intsetMoveTail操作所示)
至此,intset的删除操作全部完成了。
4、其它API
函数定义 | 实现原理 | 函数返回值 |
---|---|---|
intset *intsetNew(void); | 初始化一个intset,编码为INTSET_ENC_INT16,长度为0.content未分配空间 | intset指针 |
intset *intsetAdd(intset *is, int64_t value, uint8_t *success); | 在intset中插入指定的值。若success不为NULL,成功时置为1,若待插入的值已存在则将 success 置为0 | intset指针 |
intset *intsetRemove(intset *is, int64_t value, int *success); | 在intset中删除指定的值。若success不为NULL,成功时置为1,若待插入的值已存在,则将success置为0 | intset指针 |
uint8_t intsetFind(intset *is, int64_t value); | 在intset中查找值是否存在 | 成功返回 是否返回0 |
int64_t intsetRandom(intset *is); | 在intset中随机返回一个元素 | 随机返回其中一个元素值 |
uint8_t intsetGet(intset *is, uint32_t pos, int64_t *value); | 在intset中获取指定位置处的值。将获取的值放入value中获取intset的长度。通过intset结构体的length字段获得 | intset的长度 |
uint32_t intsetLen(const intset *is); | 和intsetGet一样 | intset的长度 |
size_t intsetBlobLen(intset *is); | 获取intset总共占用的字节数。计算方法为length * encoding + sizeof(intset),即元素个数乘以元素编码方式 + intset 结构体本身占用的字节数 | intset 共占用的字节数 |