前言:天气变冷了,人就容易变得懒惰,再加上这段时间事情还是有点多的,一下才发现,原来已有两周没有看redis源代码了,这可不行,学习还是要继续的,每天进步一点也是好的。废话不多说了,今天继续,希望自己接下来的时间能够坚持下去。
今天学习的底层数据结构比较简单,整数集合,但还是有很多值得我们学习的地方的。
4. intset ---整数集合
整数集合,顾名思义,就是用来存放整数的集合,它可以保存3种类型的整数,分别为:int16_t,int_32_t,int_64_t。
4.1 数据结构
intset的数据结构如下:
1
2
3
4
5
|
typedef
struct
intset {
uint32_t encoding;
// 编码方式
uint32_t length;
// 集合包含的元素数量
int8_t contents[];
// 保存元素的数组
} intset;
|
1
2
3
|
#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))
|
接下来就是要为这个整数集合的数据结构提供一些操作函数了,从我们前面的对sds,dict,list的分析我们大概可以看出,基本都是对数据结构中的元素进行增删改查的操作。下面我们来看一下,redis对intset提供了哪些操作,然后会分析其中的部分操作的实现
1
2
3
4
5
6
7
8
|
intset *intsetNew(
void
);
//新建一个整数集合
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);
//获取指定pos位置上的元素值
uint32_t intsetLen(intset *is);
//获取整数集合元素的个数
size_t
intsetBlobLen(intset *is);
//返回整数集合现在占用的字节总数量
|
1
2
3
4
5
6
|
intset *intsetNew(
void
) {
intset *is = zmalloc(
sizeof
(intset));
is->encoding = intrev32ifbe(INTSET_ENC_INT16);
is->length = 0;
return
is;
}
|
intsetAdd函数是添加一个元素到整数集合中去。在这里我们首先介绍一下intset是如何存储整数数据的:集合的数据是不重复的,而redis在存储的时候不仅保持了不重复,而且数据在数组中还是有序的(这样查找就会很方便)。另一个问题是,在初始化intset的时候,默认的编码方式是int16_t,如果我往里面添加元素的时候如果元素的值大于int16_t的范围应该怎么办呢?redis是通过对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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
|
/* 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)) {
/* 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;
}
/* Upgrades the intset to a larger encoding and inserts the given integer. */
static
intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
uint8_t curenc = intrev32ifbe(is->encoding);
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;
}
/* 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;
}
}
|
第5行是对标志位success默认设置为1,也就是说插入成功
第10-11行是判断是否需要进行编码升级,intsetUpgradeAndAdd是对编码进行升级并且将value添加到集合中。具体实现在31-54行。实现的思路就是:由于集合中的元素是有序的(前面已经介绍过),如果需要进行编码升级的话,说明新添加的元素是最大的,只需要将其加入到集合的最后即可。但是在这之前需要对集合中的其他元素也编码成和新添加元素同样的编码
第13-23行是对不需要进行编码升级的操作,其中17行的intsetSearch函数是用来查找元素。其具体实现是在59-98行,是不是有一种很熟悉的感觉,对,没错,就是使用的二分查找算法,所有数据结构书记里面都介绍过的查找算法。查找到了,说明元素已经存在了,success的值置为0,表示元素已经存在,没有添加成功。否则就要在22行对整数集合大小增1,然后将元素插入到相应的位子。
其他的操作基本都非常简单:intsetRemove,intsetFind都是先利用intsetSearch方法找到相应的位子,然后在进行相应的操作。在这里就不在一一列出,有兴趣的可以看看源码。
4.3 为什么要进行编码升级
如果我们一开始就对intset里面的contents定义成int64_t,这样就可以没有升级的必要了,但这样的话,有可能会对内存产生比较大的浪费,比如,在某一时刻,集合里的元素都是int16_t可以表示的数,如果用int64_t来存储的话,显然内存是int16_t来存储的4倍。
4.4 总结:从intset的实现来看,intset并不适合存在大量数据的情况下,因为为了保持集合元素的有序性,在添加元素的时候需要对元素进行移位,这个代价在素组数量比较大的情况下还是比较大的。而实际上redis也是这么做的,只有在元素数量不多的情况下才会采用这种方式作为集合的底层数据结构,一旦数据量大于某个值,就会采用dict来进行存储。