rehash过程

步骤

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

时间: 2024-10-28 21:47:16

rehash过程的相关文章

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

Redis的内存存储结构是个大的字典存储,也就是我们通常说的哈希表.Redis小到能够存储几万记录的CACHE,大到能够存储几千万甚至上亿的记录(看内存而定),这充分说明Redis作为缓冲的强大.Redis的核心数据结构就是字典(dict),dict在数据量不断增大的过程中.会遇到HASH(key)碰撞的问题,假设DICT不够大,碰撞的概率增大,这样单个hash 桶存储的元素会越来愈多,查询效率就会变慢.假设数据量从几千万变成几万,不断减小的过程.DICT内存却会造成不必要的浪费.Redis的d

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

Redis的内存存储结构是个大的字典存储,也就是我们通常说的哈希表.Redis小到可以存储几万记录的CACHE,大到可以存储几千万甚至上亿的记录(看内存而定),这充分说明Redis作为缓冲的强大.Redis的核心数据结构就是字典(dict),dict在数据量不断增大的过程中,会遇到HASH(key)碰撞的问题,如果DICT不够大,碰撞的概率增大,这样单个hash 桶存储的元素会越来愈多,查询效率就会变慢.如果数据量从几千万变成几万,不断减小的过程,DICT内存却会造成不必要的浪费.Redis的d

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

背景 Squirrel(松鼠)是美团技术团队基于Redis Cluster打造的缓存系统.经过不断的迭代研发,目前已形成一整套自动化运维体系,涵盖一键运维集群.细粒度的监控.支持自动扩缩容以及热点Key监控等完整的解决方案.同时服务端通过Docker进行部署,最大程度的提高运维的灵活性.分布式缓存Squirrel产品自2015年上线至今,已在美团内部广泛使用,存储容量超过60T,日均调用量也超过万亿次,逐步成为美团目前最主要的缓存系统之一. 随着使用的量和场景不断深入,Squirrel团队也不断

HashMap实现原理分析

1. HashMap的数据结构 数据结构中有数组和链表来实现对数据的存储,但这两者基本上是两个极端. 数组 数组存储区间是连续的,占用内存严重,故空间复杂的很大.但数组的二分查找时间复杂度小,为O(1):数组的特点是:寻址容易,插入和删除困难: 链表 链表存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达O(N).链表的特点是:寻址困难,插入和删除容易. 哈希表 那么我们能不能综合两者的特性,做出一种寻址容易,插入删除也容易的数据结构?答案是肯定的,这就是我们要提起的哈希表.

深入浅出 Java Concurrency (17): 并发容器 part 2 ConcurrentMap (2)[转]

本来想比较全面和深入的谈谈ConcurrentHashMap的,发现网上有很多对HashMap和ConcurrentHashMap分析的文章,因此本小节尽可能的分析其中的细节,少一点理论的东西,多谈谈内部设计的原理和思想. 要谈ConcurrentHashMap的构造,就不得不谈HashMap的构造,因此先从HashMap开始简单介绍. HashMap原理 我们从头开始设想.要将对象存放在一起,如何设计这个容器.目前只有两条路可以走,一种是采用分格技术,每一个对象存放于一个格子中,这样通过对格子

Redis 基础数据结构与对象

Redis用到的底层数据结构有:简单动态字符串.双端链表.字典.压缩列表.整数集合.跳跃表等,Redis并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这个系统包括字符串对象.列表对象.哈希对象.集合对象和有序结合对象共5种类型的对象. 1 简单动态字符串 redis自定义了简单动态字符串数据结构(sds),并将其作为默认字符串表示. struct sdshdr { unsigned int len; unsigned int free; char buf[

Redis一点基础的东西

目录 1.基础底层数据结构 2.windows下环境搭建 3.java里连接redis数据库 4.关于认证 5.redis高级功能总结 1.基础底层数据结构 1.1.简单动态字符串SDS 定义: struct sdshdr{ int len; int free; char buf[]; } 优势: 为了重用部分C语言函数库功能,在buf里存储了空字符'\0',但是不同于C char[]的是: 取sds长度时,直接从len中获取,不是像C中遍历buf,直到遇到空字符结束. 在改变sds内容时,如果

redis底层数据结构之dict 字典2

针对 上一文中提出的问题,这一次就进行解答: 由rehash过程可以看出,在rehash过程中,ht[0]和ht[1]同时具有条目,即字典中的所有条目分布在ht[0]和ht[1]中, 这时麻烦也就出来了.主要有以下问题:(现在暂不解答是如何解决的) 1.如何查找key. 2.如何插入新的key. 3.如何删除一个key. 4.如何确保rehash过程不断插入.删除条目,而rehash没有出错. 5.如何遍历dict所有条目,如何确保遍历顺序. 6.如何确保迭代器有效,且正确. 1. 如何查找ke

深入理解JAVA集合系列三:HashMap的死循环解读

由于在公司项目中偶尔会遇到HashMap死循环造成CPU100%,重启后问题消失,隔一段时间又会反复出现.今天在这里来仔细剖析下多线程情况下HashMap所带来的问题: 1.多线程put操作后,get操作导致死循环. 2.多线程put非null元素后,get操作得到null值. 3.多线程put操作,导致元素丢失. 死循环场景重现 下面我用一段简单的DEMO模拟HashMap死循环: 1 public class Test extends Thread 2 { 3 static HashMap<