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)函数中进行,其实现如下所示:
过程比较清楚:就是根据rehashidx得值来获取需要进行rehash的桶(期间要略过桶为空的情况),找到后,然后将整个桶的元素rehash到1号哈希表中,执行完n步后,函数返回。
我们在前面的介绍中提到的对dict的增删改查中,都提到了一个词单步rehash操作,其函数显示如下:
其内部调用的就是dictRehash函数,不过此时的n为1,所以称之为单步rehash操作,也就是一次仅仅只进行一个hash桶的元素的rehash操作。
redis将rehash的操作分散到了各个对dict的操作当中去,当然为了不影响性能,一次操作进行一次单步rehash。同样,redis也提供了在一个时间段内对dict进行rehash操作。代码如下:
ms是以毫秒为单位的,在一定的毫秒时间内,进行rehash操作。while循环中每次循环进行100步rehash操作。然后判断时间,如果超过了设定的时间,就退出。
总结:redis的rehash操作是采用渐进式的方式,rehash操作的分步主要在两方面。1.被动触发:在对dict进行增删改查操作的时候进行单步rehash;2主动触发:通过设定一定的时间进行rehash操作。
3.4. dictIterator
3.4.1 dictIterator
迭代器,用来迭代字典中的元素,在redis中,迭代器分为两种,安全迭代器和非安全迭代器。在dictIterator中用safe字段标识,再一次列出迭代器的数据结构如下所示。
安全迭代器:指的是程序在迭代的过程中,程序依然可以执行dictAdd,dictFind等操作来对字典进行修改。在上面我们介绍的_dictRehashStep(dict *d)函数,是在dictFind,dictAdd函数内部调用,该函数内部有一个判断条件,只有在字典安全迭代器的数量为0的时候才会进行单步rehash,也就是说在有安全迭代器存在的时候是不允许被动触发的单步rehash操作的。
非安全迭代器:在迭代的过程中,程序仅仅只会调用dictNext对字典进行迭代,二不会对字典进行修改。所以非安全迭代器迭代前后要对字典进行指纹计算,计算的值保存在fingerprint中。(指纹计算的方法为:取出0和1两个哈希表的table,size,used属性的值,进行一次hash操作,计算出一个特征值,迭代完后,只需要按照同样的计算方法得出的值与这个进行比较即可)
迭代器最重要的方法就是获取下一个字典的元素。其实现函数如下所示:
迭代器在第一次对字典进行迭代的时候,如果是如果为安全迭代器,字典的安全迭代器计数加1 ,如果为非安全迭代器需要计算字典的指纹。然后根据entry和nextEntry的值来获取下一个元素。在获取的过程中,如果正在rehash的话,需要切换遍历的哈希表,在代码中的注释中可以看得比较清楚。
(ps:在redis中还有一个迭代遍历函数dictScan,采用的是方向迭代的思想,在字典处于rehashing状态的时候依然能够具有很好的性能,在一些情况下会有部分数据的冗余,但不会遗漏任何一个数据。这个算法的实现确实比较精妙,有兴趣的同学可以参考http://chenzhenianqing.cn/articles/1101.html ,讲的非常精彩)
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) { //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; } |
(ps:在redis中还有一个迭代遍历函数dictScan,采用的是方向迭代的思想,在字典处于rehashing状态的时候依然能够具有很好的性能,在一些情况下会有部分数据的冗余,但不会遗漏任何一个数据。这个算法的实现确实比较精妙,有兴趣的同学可以参考http://chenzhenianqing.cn/articles/1101.html ,讲的非常精彩)
相关文章推荐
- Redis 3.0 源码解析---底层数据结构分析(2)
- Redis 3.0 源码解析---底层数据结构分析(1)
- Redis 3.0 源码解析---底层数据结构分析(4)
- Redis源码分析(1)-底层数据结构SDS
- redis源码分析(3)-- 基本数据结构双链表list
- Redis源码分析(四)——Redis数据结构-整数集合
- redis源码解析之dict数据结构
- caffe源码深入学习6:超级详细的im2col绘图解析,分析caffe卷积操作的底层实现
- Redis源码分析(一)——Redis数据结构-字符串SDS
- Redis 3.0中文官方文档翻译和源码解析
- Redis 数据结构-字典源码分析
- redis源码分析--zslRandomLevel位运算解析
- Redis源码分析-内存数据结构intset
- ECharts 3.0底层zrender 3.x源码分析3-Handler(C层)
- Redis源码分析(一)--Redis结构解析
- redis源码分析(2)-- 基本数据结构sds
- redis的源码分析之不同编码类型的数据结构
- mybatis底层源码分析之--配置文件读取和解析
- ECharts 3.0底层zrender 3.x源码分析2-Painter(V层)
- netty源码分析(二十一)Netty数据容器ByteBuf底层数据结构深度剖析与ReferenceCounted初探