您的位置:首页 > 理论基础 > 数据结构算法

Redis 3.0 源码解析---底层数据结构分析(3)

2014-11-30 19:56 726 查看
    在上一篇文章中,在对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;


}


  


        过程比较清楚:就是根据rehashidx得值来获取需要进行rehash的桶(期间要略过桶为空的情况),找到后,然后将整个桶的元素rehash到1号哈希表中,执行完n步后,函数返回。
 
      我们在前面的介绍中提到的对dict的增删改查中,都提到了一个词单步rehash操作,其函数显示如下:

1

2

3
static
 
void
 _
dictRehashStep(dict *d) {


    
if
 
(d->iterators == 0) dictRehash(d,1);


}


        其内部调用的就是dictRehash函数,不过此时的n为1,所以称之为单步rehash操作,也就是一次仅仅只进行一个hash桶的元素的rehash操作。
 
      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;


}


        ms是以毫秒为单位的,在一定的毫秒时间内,进行rehash操作。while循环中每次循环进行100步rehash操作。然后判断时间,如果超过了设定的时间,就退出。
 
      总结: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;


        安全迭代器:指的是程序在迭代的过程中,程序依然可以执行dictAdd,dictFind等操作来对字典进行修改。在上面我们介绍的_dictRehashStep(dict *d)函数,是在dictFind,dictAdd函数内部调用,该函数内部有一个判断条件,只有在字典安全迭代器的数量为0的时候才会进行单步rehash,也就是说在有安全迭代器存在的时候是不允许被动触发的单步rehash操作的。
        非安全迭代器:在迭代的过程中,程序仅仅只会调用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) { 
//0号哈希表遍历结束


                    
iter->table++; 
//table+1,下一次开始遍历1号哈希表


                    
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;


}


        迭代器在第一次对字典进行迭代的时候,如果是如果为安全迭代器,字典的安全迭代器计数加1 ,如果为非安全迭代器需要计算字典的指纹。然后根据entry和nextEntry的值来获取下一个元素。在获取的过程中,如果正在rehash的话,需要切换遍历的哈希表,在代码中的注释中可以看得比较清楚。

        
        (ps:在redis中还有一个迭代遍历函数dictScan,采用的是方向迭代的思想,在字典处于rehashing状态的时候依然能够具有很好的性能,在一些情况下会有部分数据的冗余,但不会遗漏任何一个数据。这个算法的实现确实比较精妙,有兴趣的同学可以参考http://chenzhenianqing.cn/articles/1101.html ,讲的非常精彩)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息