步骤

1) 首先创建一个比现有哈希表更大的新哈希表(expand)
2) 然后将旧哈希表的所有元素都迁移到新哈希表去(rehash)
 

dictAdd 对字典添加元素的时候, _dictExpandIfNeeded 会一直对 0 号哈希表的使用情况进行检查。
当 rehash 条件被满足的时候,它就会调用 dictExpand 函数,对字典进行扩展。
static int _dictExpandIfNeeded(dict *d)  
{  
    // 当 0 号哈希表的已用节点数大于等于它的桶数量,  
    // 且以下两个条件的其中之一被满足时,执行 expand 操作:  
    // 1) dict_can_resize 变量为真,正常 expand  
    // 2) 已用节点数除以桶数量的比率超过变量 dict_force_resize_ratio ,强制 expand  
    // (目前版本中 dict_force_resize_ratio = 5)  
    if (d->ht[0].used >= d->ht[0].size &&    (dict_can_resize ||  d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))  
    {  return dictExpand(d, ((d->ht[0].size > d->ht[0].used) ?   d->ht[0].size : d->ht[0].used)*2);  }  
}
 

将新哈希表赋值给 1 号哈希表,并将字典的 rehashidx 属性从 -1 改为 0:
int dictExpand(dict *d, unsigned long size)  
{  
    // 被省略的代码...  
  
    // 计算哈希表的(真正)大小  
    unsigned long realsize = _dictNextPower(size);  
  
    // 创建新哈希表  
    dictht n;  
    n.size = realsize;  
    n.sizemask = realsize-1;  
    n.table = zcalloc(realsize*sizeof(dictEntry*));  
    n.used = 0;  
  
    // 字典的 0 号哈希表是否已经初始化?  
    // 如果没有的话,我们将新建哈希表作为字典的 0 号哈希表  
    if (d->ht[0].table == NULL) {  
        d->ht[0] = n;  
    } else {  
    // 否则,将新建哈希表作为字典的 1 号哈希表,并将它用于 rehash  
        d->ht[1] = n;  
        d->rehashidx = 0;  
    }  
  
    // 被省略的代码...  
}  
 

 

渐增式rehash和平摊操作

集中式的 rehash 会引起大量的计算工作。
渐增式 rehash将 rehash 操作平摊到dictAddRaw 、dictGetRandomKey 、dictFind 、dictGenericDelete这些函数里面,每当上面这些函数被执行的时候, _dictRehashStep 函数就会执行,将 1 个元素从 0 号哈希表 rehash 到 1 号哈希表,这样就避免了集中式的 rehash 。

以下是 dictFind 函数,它是其中一个平摊 rehash 操作的函数:
dictEntry *dictFind(dict *d, const void *key)  
{  
    // 被忽略的代码...  
  
    // 检查字典(的哈希表)能否执行 rehash 操作  
    // 如果可以的话,执行平摊 rehash 操作  
    if (dictIsRehashing(d)) _dictRehashStep(d);  
  
    // 被忽略的代码...  
}  
  
其中 dictIsRehashing 就是检查字典的 rehashidx 属性是否不为 -1 :#define dictIsRehashing(ht) ((ht)->rehashidx != -1)  
如果条件成立成立的话, _dictRehashStep 就会被执行,将一个元素从 0 号哈希表转移到 1 号哈希表:
static void _dictRehashStep(dict *d) {    if (d->iterators == 0) dictRehash(d,1);  }  
  
(代码中的 iterators == 0 表示在 rehash 时不能有迭代器,因为迭代器可能会修改元素,所以不能在有迭代器的情况下进行 rehash 。)

0 号哈希表的元素被逐个逐个地,从 0 号 rehash 到 1 号,最终整个 0 号哈希表被清空,这时 _dictRehashStep 再调用 dictRehash ,被清空的 0 号哈希表就会被删除,然后原来的  1 号哈希表成为新的 0 号哈希表。

当 rehashidx 不等于 -1 ,也即是 dictIsRehashing 为真时,所有新添加的元素都会直接被加到 1 号数据库,这样 0 号哈希表的大小就会只减不增。


哈希表的大小

我们知道哈希表最初的大小是由 DICT_HT_INITIAL_SIZE 决定的,而当 rehash 开始之后,根据给定的条件,哈希表的大小就会发生变动:
 
static int _dictExpandIfNeeded(dict *d)  
{  
    // 被省略的代码...  
  
    if (d->ht[0].used >= d->ht[0].size &&  
        (dict_can_resize ||  
         d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))  
    {  
        return dictExpand(d, ((d->ht[0].size > d->ht[0].used) ?  
                                    d->ht[0].size : d->ht[0].used)*2);  
    }  
  
    // 被省略的代码...  
}  
 
可以看到, d->ht[0].size 和 d->ht[0].used 两个数之间的较大者乘以 2 ,会作为 size 参数被传入 dictExpand 函数,但是,尽管如此,这个数值仍然还不是哈希表的最终大小,因为在 dictExpand 里面,_dictNextPower 函数会根据传入的 size 参数计算出真正的表大小:
 
int dictExpand(dict *d, unsigned long size)  
{  
    // 被省略的代码...  
  
    // 计算哈希表的(真正)大小  
    unsigned long realsize = _dictNextPower(size);  
  
    // 创建新哈希表  
    dictht n;  
    n.size = realsize;  
    n.sizemask = realsize-1;  
    n.table = zcalloc(realsize*sizeof(dictEntry*));  
    n.used = 0;  
  
    // 被省略的代码...  
}  
 
至于 _dictNextPower 函数,它不断计算 2 的乘幂,直到遇到大于等于 size 参数的乘幂,就返回这个乘幂作为哈希表的大小:
 
static unsigned long _dictNextPower(unsigned long size)  
{  
    unsigned long i = DICT_HT_INITIAL_SIZE;  
  
    if (size >= LONG_MAX) return LONG_MAX;  
    while(1) {  
        if (i >= size)  
            return i;  
        i *= 2;  
    }  
}

1) 哈希表的大小总是 2 的乘幂(也即是 2^N,此处 N 未知)
2)1 号哈希表的大小总比 0 号哈希表大


最后, 我为 redis 的源码分析项目专门建立了一个 github project ,上面有完整的源码文件,大部分加上了注释(目前只有 dict.c 和 dict.h),如果对代码的完整细节有兴趣,可以到上面去取:  https://github.com/huangz1990/reading_redis_source
 

rehash过程的更多相关文章

  1. Redis的字典(dict)rehash过程源代码解析

    Redis的内存存储结构是个大的字典存储,也就是我们通常说的哈希表.Redis小到能够存储几万记录的CACHE,大到能够存储几千万甚至上亿的记录(看内存而定),这充分说明Redis作为缓冲的强大.Re ...

  2. redis的rehash过程

    在扩容和收缩的时候,如果哈希字典中有很多元素,一次性将这些键全部rehash到ht[1]的话,可能会导致服务器在一段时间内停止服务.所以,采用渐进式rehash的方式,详细步骤如下: 为ht[1]分配 ...

  3. 关于Redis的启动过程

    一.简介 Redis的启动也就是main函数的执行,程序的入口在redis.c中,启动流程: 1. 初始化默认服务器配置,如果是sentinel模式还需进行额外的配置 2. 修改配置文件或配置选项,这 ...

  4. 美团针对Redis Rehash机制的探索和实践

    背景 Squirrel(松鼠)是美团技术团队基于Redis Cluster打造的缓存系统.经过不断的迭代研发,目前已形成一整套自动化运维体系,涵盖一键运维集群.细粒度的监控.支持自动扩缩容以及热点Ke ...

  5. redis渐进式 rehash

    转载(http://redisbook.com/preview/dict/incremental_rehashing.html) 上一节说过, 扩展或收缩哈希表需要将 ht[0] 里面的所有键值对 r ...

  6. 《闲扯Redis八》Redis字典的哈希表执行Rehash过程分析

    一.前言 随着操作的不断执行, 哈希表保存的键值对会逐渐地增多或者减少, 为了让哈希表的负载因子(load factor)维持在一个合理的范围之内, 当哈希表保存的键值对数量太多或者太少时, 程序需要 ...

  7. redis字典快速映射+hash釜底抽薪+渐进式rehash | redis为什么那么快

    前言 相信你一定使用过新华字典吧!小时候不会读的字都是通过字典去查找的.在Redis中也存在相同功能叫做字典又称为符号表!是一种保存键值对的抽象数据结构 本篇仍然定位在[redis前传]系列中,因为本 ...

  8. erlang 分布式数据库Mnesia 实现及应用

    先推荐一篇:mnesia源码分析(yufeng)   - linear hash   ETS/DETS/mnesia 都使用了linear hash算法 http://en.wikipedia.org ...

  9. HashMap实现原理分析(转)

    文章转自:http://blog.csdn.net/vking_wang/article/details/14166593 1. HashMap的数据结构 数据结构中有数组和链表来实现对数据的存储,但 ...

随机推荐

  1. 详解jquery插件中(function ( $, window, document, undefined )的作用。

    1.(function(window,undefined){})(window); Q:(function(window,undefined){})(window);中为什么要将window和unde ...

  2. Python2 和 Python3 区别汇总

    [Python2 和 Python3 的区别汇总,不定期补充] print 在进行程序调试时用得最多的语句可能就是 print,在 Python 2 中,print 是一条语句,而 Python3 中 ...

  3. where VS having

    where 和 having 的区别:        WHERE 子句不能包含聚集函数: 因为试图用聚集函数判断那些行输入给聚集运算是没有意义的.相反,HAVING 子句总是包含聚集函数    hav ...

  4. If you ever have a broken heart

    If you ever have a broken heart Pablo Neruda Tonight i can write the saddest lines Write ,for exampl ...

  5. python数据之间的转换和关系

    首先数据类型在我看来分为两类: 容器类:能存储数据,例如:元祖.列表.集合.字符串. 原子类:单纯保存数值,例如:整数.浮点数.复数. 容器类与容器类之间,一般都可以进行两两之间的转化. 原子类与原子 ...

  6. PDO:数据访问抽象层

    <?php //PDO:数据访问抽象层 //带有事务功能: //dsn:数据源 $dsn="mysql:host=localhost;dbname=aaas"; //造pdo ...

  7. pyhton3 logging模块

    1.简单的将日志打印到屏幕   import logging logging.debug('This is debug message')logging.info('This is info mess ...

  8. POJ 3468 A Simple Problem with Integers 【线段树】

    题目链接 http://poj.org/problem?id=3468 思路 线段树 区间更新 模板题 在赋初始值的时候,按点更新区间就可以 AC代码 #include <cstdio> ...

  9. Loadrunder之脚本篇——参数类型

    Internal data Date/Time,Group Name,Iteration Number,Load Generator Name,Ramdom Number,Table,Unique N ...

  10. OC知识点(类方法,构造方法,组合模式,get,set方法,自动生成属性)

    1.类方法的优势 不用创建对象,节省了空间,直接用类名调用类方法,类方法为外界提供一个方便的调用接口.(特点:类方法以加号开头,不能使用自身的成员变量,它的调用不依赖成员变量) 2.构造方法(初始化成 ...