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

本文详细解析Redis的渐进式rehash操作,探讨何时进行rehash以及如何分步进行。Redis在字典达到一定负载比时启动rehash,采用单步rehash策略,将影响分散到多个操作中,以减少对性能的影响。此外,文章还介绍了安全和非安全迭代器的概念,确保在迭代过程中对字典的操作不会破坏rehash流程。
摘要由CSDN通过智能技术生成
    在上一篇文章中,在对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 ,讲的非常精彩
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值