|
最近,我想通过redis的源码来学习redis。虽然平时工作中用得不多,不过对redis还是比较感兴趣的,毕竟它的性能是不错的。redis是一个开源的项目,我们可以通过源代码去了解redis。我后面会通过自己的学习,写一些关于redis源码的帖子。帖子的主要内容是分析代码设计,而并不会对源码进行详细解说。如果有不对的地方,请指正。源码是reids 3.0.3版本。
intset
一、intset数据结构
intset,是用于存储整数集合的数据结构。set的特点是无重复元素,元素可以无序。不过redis的intset是一个元素有序的数据结构。
先看intset的定义:
1
2
3
4
5
| typedef struct intset {
uint32_t encoding; //整型编码类型
uint32_t length; //intset大小,元素个数
int8_t contents[]; //数据存储区
} intset;
|
先结合intset的相关操作,再来谈intset的特点。下面通过部分代码来说明intset的行为特点。
二、inetset提供的相关操作函数
intset对外暴露的函数:
1
2
3
4
5
6
7
8
| intset *intsetNew(void); //创建一个空的intset
intset *intsetAdd(intset *is, int64_t value, uint8_t *success); //插入元素
intset *intsetRemove(intset *is, int64_t value, int *success); //删除元素
uint8_t intsetFind(intset *is, int64_t value); //查找元素
int64_t intsetRandom(intset *is); //随机选取元素
uint8_t intsetGet(intset *is, uint32_t pos, int64_t *value); //获取指定位置的元素
uint32_t intsetLen(intset *is); //获取intset元素个数
size_t intsetBlobLen(intset *is); //获取intset所用空间大小
|
1. 整型编码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| /* 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))
/* Return the required encoding for the provided value. */
static uint8_t _intsetValueEncoding(int64_t v) {
if (v < INT32_MIN || v > INT32_MAX)
return INTSET_ENC_INT64;
else if (v < INT16_MIN || v > INT16_MAX)
return INTSET_ENC_INT32;
else
return INTSET_ENC_INT16;
}
|
支持int16_t, int32_t, int64_t 四种类型的存储。每种类型的值为其空间大小,这样定义可以满足 INTSET_ENC_INT16 < INTSET_ENC_INT32 < INTSET_ENC_INT64,后面类型的值域包含前面的值域。
注:_intsetValueEncoding 函数声明为 static,是文件内部的函数,外部不可见。
2. 获取指定位置的元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| /* Return the value at pos, given an encoding. */
static int64_t _intsetGetEncoded(intset *is, int pos, uint8_t enc) {
int64_t v64;
int32_t v32;
int16_t v16;
//按照传入的编码类型去解析数据元素的数组
//intset是按小端进行存储的,所以还有可能对获取的元素进行大端转小端的转换
if (enc == INTSET_ENC_INT64) {
memcpy(&v64,((int64_t*)is->contents)+pos,sizeof(v64));
memrev64ifbe(&v64);
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) {
//把intset记录的编码类型传入函数
return _intsetGetEncoded(is,pos,intrev32ifbe(is->encoding));
}
|
虽然intset内部对整数进行编码存储,对外暴露的都是int64_t类型。
内部通过对数据进行编码存储,有可能节省存储空间。
因为intset是小端存储的,所以读取或写入数据时,都需要进行大小端转换。但目前我还没有明白这样做的必要,因为觉得intset只是内部的数据结构,可以不用硬性要求用大小端存储。
3. 插入元素(可能引起intset重组)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| /* 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. */
if (valenc > intrev32ifbe(is->encoding)) {
//如果value的编码类型比当前intset的编码类型要大,元素一定不在intset中,
//需要插入,同时需要扩展intset的编码类型。升级intset的编码类型时,
//动态申请更大的空间以足够存储扩展后的length+1个元素,
//具体参见 intsetUpgradeAndAdd实现
/* 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)) {
if (success) *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;
}
|
需要注意的是,如果插入的数据的编码类型比当前intset的编码类型要大,为了能存储新元素,intset需要进行类型扩展。扩展类型时会把intset中原有的所有元素都扩展成新的类型,这样就需要重组intset,当原来元素个数比较多重组会稍费时。不过intset最多会进行两次重组。因为有可能进行重组,这也是intset不适合存储大量元素的原因之一。redis中默认配置intset元素个数达512时,就会采用另外的数据结构来存储。
4. 查找元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
| /* Search for the position of "value". Return 1 when the value was found and
* sets "pos" to the position of the value within the intset. Return 0 when
* the value is not present in the intset and sets "pos" to the position
* where "value" can be inserted. */
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,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) {
if (pos) *pos = mid;
return 1;
} else {
if (pos) *pos = min;
return 0;
}
}
/* Determine whether a value belongs to this set */
uint8_t intsetFind(intset *is, int64_t value) {
uint8_t valenc = _intsetValueEncoding(value);
//如果value的编码类型大于intset的编码类型,则value一定不存在于intset中
return valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,NULL);
}
|
查找的时间复杂度是O(logN)的,对于N比较小时是可以接受的,但当N比较大时,如 1000000时,比较次数就可能达到20次了。
5. 删除元素(不会引起intset重组)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| /* 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;
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;
}
|
intset在删除元素时是不会进行元素的数据类型检查,以降级intset的整型类型,intset也不另外提供这样的能力。原因我想大致有:a.需要扫描所有元素以确定目前是否需要进行编码类型降级。b.降级编码类型会引起重组。c.降级后可能需要再进行升级,如果不幸发生频繁升级降级重组,性能不稳定。
结合插入删除函数,都使用了intsetResize,里面中使用了zrealloc来进行内存空间的增减。在增大空间时要么重新分配一块新的空间,要么在原有的空间的最后进行扩展。减小空间时只需在原有的空间最后进行减小即可。这样的方式不太容易产生内存碎片。
三、特点(优缺点需要进行比较才能显出来,由于这里没有比较,优缺点不明显,所以这里只列举了特点):
1.有序数组,存储元素紧密,空间利用率高,而且不容易因频繁地插入删除而产生内存碎片。
2.支持整型编码,intset中所有数据元素的存储类型是一致的。新插入数据时,如果数据的类型大于当前intset的数据类型,则可扩大存储的整型类型(但intset不提供缩小编码类型的能力)。在一定程序上可节省空间,但所有的存取都需要考虑类型,增加代码的复杂度。
3.有长度字段可记录元素个数,取元素个数操作为常量。
4.有序数组,查找元素O(logN)
5.插入元素时,可能需要移动至多N个元素,也有可能使编码类型升级,重组intset。由于这两个特点,intset并不适合于频繁插入和存储大量元素。
6.删除元素时,可能需要移动至多N-1个元素。
7.以小端形式存储数据,包括encoding,length和数据元素。
|
|