在上一篇文章中,在对dict的add,update,find,delete等操作中多次提到了一个词单步渐进式rehash操作,这篇文章我们也来看看redis是如何对字典进行rehash操作的,同时对字典的遍历进行相关的解读。
3.3.dict---渐进式rehash
在Redis哈希表数据结构中,由于采用的是数组实现哈希表,利用链表来解决哈希冲突,必然会存在一个问题,当哈希表的大小不能满足需求,如已经有1024个元素了,但是我的字典中桶的大小的只有128,造成过多的哈希冲突,这显然影响了字典的快速操作元素(平均操作一个元素,光定位这个元素就得需要8次),使得性能降低,那么就需要重新来申请一个较大的(如2048个桶大小的哈希表),然后将之前的字典中的元素重新hash到这个新的哈希表中,我们称这一过程为rehash。
那么问题来了,什么时候进行rehash呢?其实,在上一篇文章中我们已经介绍了redis进行rehash的条件有两个:
1.字典已使用节点数大于字典大小,也可以说两者的比率接近1:1
2.字典可以被rehash(指dict_can_resize) 或者字典中使用节点数和字典大小之间的比率超过dict_force_resize_ratio
dict_force_resize_ratio=5,在dict.c中定义.dict_can_resize=1,同样也在dict.c中定义。dict_can_resize指的是可以手动开启和关闭字典的rehash操作。但是这并不是一定的,因为,当达到一定条件:字典中使用节点数和字典大小之间的比率超过 dict_force_resize_ratio的话,都会强制进行rehash。
那么,如果来对字典进行rehash呢?有两种办法
1.当需要进行rehash操作的时候,我们创建新的哈希表的时候,直接将所有的元素rehash到新的哈希表中,有一点是在进行扩容的时候需要将哈希表锁定,不能有其他操作,rehash完成之后再进行其他的操作,这在数据量较小的情况下是没有问题的,但是如果数据量很大之后,就会很大的影响,在rehash的时候,外界不能对字典做任何操作。就好像你访问一个网页,如果这个网页10秒钟才打开,你会怎么想,哎,算了,这个网站肯定挂了,然后直接关掉了。Java的hashmap就是这么做的,所以在很多关于hashmap的说明中,如果你事先知道会用到大概多少元素,最好提前指定,这样做显然是为了避免rehash操作给hashmap带来的性能开销。
2.上面的方法是一次性的将所有的字典元素rehash到新的哈希表中,另一种办法自然就是渐进式rehash,也就是分步进行了,既然一次性rehash影响到性能,那就分步进行,将rehash的操作分散到一个一个小的时间单元里,一次仅仅只rehash一小部分,或者当服务器空闲的时候进行rehash操作。
redis采用的第二种方法,分步进行rehash操作。那么对于一个字典来说,有两种状态,正在rehash状态,非rehash状态(rehash完成或者没有rehash)。所以,在redis的dict中定义了一个rehashidx来表明这两种状态。rehashidx=-1,非rehash状态;rehashidx>=0,rehash状态,并且rehashidx的值表明了当前已经rehash到哪个桶了。
redis的rehash的过程在int dictRehash(dict *d, int n)函数中进行,其实现如下所示:
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
|
/*
** d---要进行rehash的字典
** n---对字典进行n步渐进式rehash操作,n指的是哈希的n个桶
** 也就是说rehash的最小单位为桶
**/
int
dictRehash(dict *d,
int
n) {
if
(!dictIsRehashing(d))
return
0;
while
(n--) {
//进行n步渐进式rehash
dictEntry *de, *nextde;
if
(d->ht[0].used == 0) {
// 如果 0 号哈希表为空,表示 rehash 执行完毕
zfree(d->ht[0].table);
d->ht[0] = d->ht[1];
_dictReset(&d->ht[1]);
d->rehashidx = -1;
return
0;
}
assert
(d->ht[0].size > (unsigned)d->rehashidx);
while
(d->ht[0].table[d->rehashidx] == NULL) d->rehashidx++;
de = d->ht[0].table[d->rehashidx];
while
(de) {
unsigned
int
h;
nextde = de->next;
h = dictHashKey(d, de->key) & d->ht[1].sizemask;
de->next = d->ht[1].table[h];
d->ht[1].table[h] = de;
d->ht[0].used--;
d->ht[1].used++;
de = nextde;
}
d->ht[0].table[d->rehashidx] = NULL;
d->rehashidx++;
}
return
1;
}
|
我们在前面的介绍中提到的对dict的增删改查中,都提到了一个词单步rehash操作,其函数显示如下:
1
2
3
|
static
void
_
dictRehashStep(dict *d) {
if
(d->iterators == 0) dictRehash(d,1);
}
|
redis将rehash的操作分散到了各个对dict的操作当中去,当然为了不影响性能,一次操作进行一次单步rehash。同样,redis也提供了在一个时间段内对dict进行rehash操作。代码如下:
1
2
3
4
5
6
7
8
9
|
int
dictRehashMilliseconds(dict *d,
int
ms) {
long
long
start = timeInMilliseconds();
// 记录开始时间
int
rehashes = 0;
while
(dictRehash(d,100)) {
rehashes += 100;
if
(timeInMilliseconds()-start > ms)
break
;
// 如果时间已过,跳出
}
return
rehashes;
}
|
总结:redis的rehash操作是采用渐进式的方式,rehash操作的分步主要在两方面。1.被动触发:在对dict进行增删改查操作的时候进行单步rehash;2主动触发:通过设定一定的时间进行rehash操作。
3.4. dictIterator
3.4.1 dictIterator
迭代器,用来迭代字典中的元素,在redis中,迭代器分为两种,安全迭代器和非安全迭代器。在dictIterator中用safe字段标识,再一次列出迭代器的数据结构如下所示。
1
2
3
4
5
6
7
8
9
10
11
|
typedef
struct
dictIterator {
dict *d;
//指向要迭代的字典
long
index;
//迭代器当前所指向的哈希表索引位置
//table:正在被迭代的hash表,dict中申请了两个hash表,值可以为0或1
//safe:表示这个迭代器是否安全
int
table, safe;
//entry:指向当前迭代到的节点指针
//nextEntry:指向下一个迭代节点的指针
dictEntry *entry, *nextEntry;
long
long
fingerprint;
//用于非安全迭代器计算字典指纹
} dictIterator;
|
非安全迭代器:在迭代的过程中,程序仅仅只会调用dictNext对字典进行迭代,二不会对字典进行修改。所以非安全迭代器迭代前后要对字典进行指纹计算,计算的值保存在fingerprint中。(指纹计算的方法为:取出0和1两个哈希表的table,size,used属性的值,进行一次hash操作,计算出一个特征值,迭代完后,只需要按照同样的计算方法得出的值与这个进行比较即可)
迭代器最重要的方法就是获取下一个字典的元素。其实现函数如下所示:
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
|
dictEntry *dictNext(dictIterator *iter)
{
while
(1) {
if
(iter->entry == NULL) {
dictht *ht = &iter->d->ht[iter->table];
//第一次迭代时才会执行这个if语句,如果为安全迭代器,字典的安全迭代器计数加1
//如果为非安全迭代器需要计算字典的指纹
if
(iter->index == -1 && iter->table == 0) {
if
(iter->safe)
iter->d->iterators++;
else
iter->fingerprint = dictFingerprint(iter->d);
}
iter->index++;
//判断哈希表是否迭代完
if
(iter->index >= (
long
) ht->size) {
if
(dictIsRehashing(iter->d) && iter->table == 0) {
iter->table++;
iter->index = 0;
ht = &iter->d->ht[1];
}
else
{
break
;
}
}
iter->entry = ht->table[iter->index];
}
else
{
iter->entry = iter->nextEntry;
}
if
(iter->entry) {
/* We need to save the 'next' here, the iterator user
* may delete the entry we are returning. */
iter->nextEntry = iter->entry->next;
return
iter->entry;
}
}
return
NULL;
}
|
(ps:在redis中还有一个迭代遍历函数dictScan,采用的是方向迭代的思想,在字典处于rehashing状态的时候依然能够具有很好的性能,在一些情况下会有部分数据的冗余,但不会遗漏任何一个数据。这个算法的实现确实比较精妙,有兴趣的同学可以参考
http://chenzhenianqing.cn/articles/1101.html ,讲的非常精彩
)