哈希表的扩容机制

本文介绍了哈希表这一常见数据结构,其查找、新增和删除元素性能较好。借助Java的HashMap和Redis的哈希表,阐述了哈希表实现的三要素,包括数组、映射函数和解决冲突的方法。还分别说明了HashMap和Redis的rehash时机与过程,其中Redis采用渐进式rehash。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文主要内容:

哈希表介绍

Java的扩容机制

Redis的扩容机制

一、哈希表的介绍

        哈希表,又称散列表,是一种常见的数据结构。通过哈希表使用某个键值可以快速定位到数据在内存中的位置。比如我们使用新华字典时,前面会有很多页的索引,我们要查找”哈“这个字,会找到拼音首字母是h的,然后查找对应的页码,这样可以快速查到对应页,不必再一页一页翻。哈希表的应用是非常广泛的,如果你之前学过操作系统,一定接触过磁盘缓冲区,bufferCache(PageCache),其内部就使用了哈希表。

        对于数组和链表,当要查找一个元素时,需要从头遍历数组(不考虑利用索引下标查找)和链表来查找某个元素,最差和最优的时间复杂度都是O(N);在查找和删除时,同样时间复杂度也较高。Java开发者对List的两种实现形式ArrrayList和LinkedList比较熟悉。ArrayList在插入元素时,如果不是插入到数组尾部,还要进行数组的移动,LinkedList在添加元素如果不是尾部,也需要进行遍历,虽然不需要数组移动,但也需要O(N)的时间复杂度。而哈希表在查找、新增和删除元素时性能较好,正常情况下查找某个元素的时间复杂度只有O(1),当然在最差的情况下也有可能是O(N),这个后续再说。

本文主要会借助Java的HashMap以及Redis的哈希表进行介绍。

哈希表的实现

实现哈希表最关键的三要素:

1、数组,哈希表的底层实现数据结构;

2、映射函数,通过映射把某个键值映射到数组的某个索引下标;

3、解决冲突;

        在创建哈希表时,会先初始化一个数组,此外定义一个散列函数f(key),通过散列函数可以得到某个索引下标值,随后将元素放到对应索引上。如果对应元素已经有值,就证明出现了哈希碰撞,此时会借助其他方式来解决冲突。

1)数组

        哈希表的底层是通过数组实现的,但其不会像数组一样按顺序存储数据,而是根据某种规则来实现的。也就是说数组中可能有些位置并没有值。

        既然使用了数组,那就涉及到初始化。初始化最重要的是容量的大小的确定,要适中,过大会导致空间的浪费,过小会导致频繁的扩容,影响性能。看一下Java的HashMap和Redis的哈希表数组的初始化。

HashMap数组:

如果不指定数组大小,HashMap的数组默认大小是16,装载因子是0.75

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    transient Node<K,V>[] table;

如果指定了初始大小,HashMap会向上取第一个2的倍数值,比如指定大小为7,则初始大小就是8.具体实现代码:

 static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

上面代码主要做了以下几步:

1、减1,主要是为了兼容当初始容量本身就是2的倍数的情况,比如8,二进制是1000,如果不减去1,下面的各种二进制操作之后会得到16,这不是我们要的结果。为了能得到8本身,就先减去1,变成0111,再进行位运算。

2、位运算,目的就是将所有的二进制位数都变成1。该函数中进行了5次位运算,保证在整型范围内的任何一个元素,经过位运算后的数据都会变成全1,因为整型是4字节,32比特位。

3、最后加1,即全1的数+1,则向上进1从而得到2的倍数。

HashMap之所以追求容量是2的倍数,是因为其散列函数决定的。HashMap在计算索引下标时会经过如下位运算:

    tab[i = (n - 1) & hash],//n表示的就是容量大小

        如果都是2的幂次,n-1获得的就是全1比特位,与hash值进行与运算之后,hash本身的散列值可以均匀的分布在整个数组中。反之,如果不是2的幂次,则有可能因为与运算导致hash冲突。举个例子。如容量是7. n-1是6,二进制是 0110,如果有两个Key的hash值是0010,0011,分别与0110进行与运算

0010 & 0110 => 0010,

0011 & 0110 => 0010.

可以看到,不同的hash值进行与运算之后,索引冲突了。而如果容量是2的幂次,可以一定程度上避免冲突的发生。

Redis数组

        说完HashMap,再说下Redis哈希表中的数组。

        Redis中有两个地方是以hash表的形式存在,一是整个Redisdb是一个大的哈希表,key-value的格式,可以看到其源码中有关于RedisDb的结构体定义(Redis是C语言实现的,看过他的代码时,我想说这是我见过的写得最好的C语言系统,可能我见识短浅):

typedef struct redisDb {
    dict *dict;    
    ......
} redisDb;

        这个dict就是一个大的map,此外hash结构中的value数据也是使用map结构,Redis中都叫做字典(和python一个叫法),dict的底层数据结构会有两个数组,即二维数组。

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];    //共两个dictht
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    unsigned long iterators; /* number of iterators currently running */
} dict;

        上面的dictht是一个具有两个元素的数组,这个主要用于rehash过程,后面会说。数组中的元素是dictht,dictht是一个结构体:

typedef struct dictht {
    dictEntry **table;     //真实存储数据的数组
    unsigned long size;   //数组容量,和hasmap的容量一直
    unsigned long sizemask;  
    unsigned long used; //实际使用大小
} dictht;

        dictht同样是一个数组,默认初始化容量大小是4,其额外多了一个数组。dictEntry类似于HashMap的Node,具备后继指针,指向后面的节点。

typedef struct dictEntry {
    void *key;   //实际key值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
  
    struct dictEntry *next;   //指向后面的节点
} dictEntry;

2) 映射函数

        也称作散列函数,通过散列函数可以算出键值的hash值,随后经过与容量进行与运算得到数组索引下标。

        映射函数非常重要,其衡量指标是不同的key得到相同的散列值的概率越小越小。现在我们看看HashMap和Redis都分别如何实现的。

HashMap散列函数:

 static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

以上代码清晰明了:

1、key可以为null,若为null,直接返回0;

2、让key的hashcode的高16位与低16位进行与运算,让高16位也参与运算的目的就是能利用hash值得更多信息,从而尽量避免冲突得发生。如两个key的hash值是1111001,1001001(我这里就不弄32位了,太多了,就是举个例子),数组容量是4,hash & (4-1)->hash & 0011。这两个进行与运算之后得到的索引是一样的,于是出现了冲突,如果可以利用高位,结果就不一样了。

3、字符串的hashcode计算方法是:s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1],n表示字符串长度。

Redis散列函数:

//宏定义了hash算法
#define dictHashKey(d, key) (d)->type->hashFunction(key)

//算出索引下标。通常这个子网掩码也是数组容量-1
idx = hash & d->ht[table].sizemask

Redis共有三个hash算法:

1)Thomas Wang’s 32 bit Mix函数

2)MurmurHash2算法

3)使用基于djb哈希的一种简单的哈希算法

如果你对hash算法比较感兴趣,可以移步到参考资料的《Redis hash详解》一篇。

3)hash碰撞

        hash碰撞是哈希表中不可避免的现象,不管你的散列函数设计得如何好,都不能完全避免,因为我们的数组长度是有限的。

        既然无法避免,就应该有相应的解决办法。目前已知的解决hash碰撞的方法包括:

  • 开放地址法

        它的思想是当遇到冲突时,根据某种规则寻找下一个空的位置。规则包括线性探测、二次探测以及伪随机探测等;这种算法不是很常用.

  • 再hash法,也称再散列法

        当遇到冲突时,再计算一次hash值,直到没有冲突位置;

  • 链地址法

        当遇到冲突时,在冲突的位置在额外构造出一个链表,所有hash冲突的key都放到一个链表上。这也是HashMap和Redis都采用的解决hash碰撞的方法。链地址法还包括头插法和尾插法。即冲突的数据是加在链表头部还是尾部。

Redis采用的头插法:

dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{
    //根据是否正在hash判断
    ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
    entry = zmalloc(sizeof(*entry));
    entry->next = ht->table[index];
    ht->table[index] = entry;
    ht->used++;

    /* Set the hash entry fields. */
    dictSetKey(d, entry, key);
    return entry;
}

上述代码主要做了以下几个事情:

1、判断是否正在rehash,如果是,就直接放到h[1]中,否则就是h[0]中;

2、直接将元素放在当前数组索引位置,下一个节点指向插入前的头节点;

3、设置field值。

从上述代码可以清晰地看见,其采用了头插法。

HashMap:

        在JDK1.7中,使用的头插法,在JDK1.8中采用的是尾插法。且1.8中分别使用链表和红黑树一起来存储不同数量的冲突元素。下面是链表的结构,红黑树的就不画了:

        之所以在JDK1.8中链表采用尾插法主要是解决多线程场景下出现的死循环问题。但我想说,本身HashMap就不是线程安全的,因此在多线程的场景下就不应该使用HashMap,而是使用ConcurrentHashMap,所以头插法并不是问题的本质,问题的本质是不能在多线程编程中去使用一个并不是线程安全的数据结构。不过对这方面感兴趣的可自行google一下。

        在JDK1.8中,当冲突元素数量较少时,会使用链表存储冲突元素,且新元素加在末尾;当元素数量较多时,链表会转变为红黑树,目的就是为了提高增删改查的效率。链表和红黑树的转换的边界是内部设置了一个阈值:

    static final int TREEIFY_THRESHOLD = 8;

        当冲突元素数量超过这个阈值时,就会将链表转换为红黑树。

        HashMap的添加元素的操作如下:

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

putVal是实际的操作。最关键的几步:

1、计算索引下标;

2、判断当前位置是否有值,如果没有直接新创建一个节点;

3、如果没有,则判断当前冲突是红黑树还是链表。随后根据具体数据结构来进行添加;

4、如果是链表,还要看看是否需要转换为红黑树

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //对应索引位置是否为空
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
           //不是空
            Node<K,V> e; K k;
            //第一个if意思是完全是对应的元素,直接覆盖
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //如果是红黑树,则按照红黑树结构插入(本文不介绍)
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                    //链表结果,插入到链表尾部
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //插入之后判断是否需要转换为红黑树(注意,这里并不一定会进行实际的红黑树演进
                       //这个后面会说
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }  
        }
    }

        可以看到,HashMap采用了链表尾部插入+红黑树两种结构实现的,Redis是链表头插法。那Redis效率是不是很差?其实不然,首先Redis适用了头插法,插入速度较快。其次虽然查找和删除效率较差,但Redis的冲突元素如果很多时,会进行rehash。其实Hashmap之前也是只采用链表的头插法来解决冲突的,只是因为多线程的死循环总被人诟病,Java开发者有点受不了这些人了(刚开始他们一直在强调,这不是hashmap的错,而是你在并发场景使用线程不安全的数据结构才产生的)。

        下一部分是哈希表的rehash的过程。首先要知道为什么要rehash?rehash的目的是为了解决冲突元素过多以及容量不足的问题。我们分别看下HashMap以及Redis是如何实现的。

二、HashMap的rehash

HashMap发生rehash的时机都是在进行put操作时才会触发,其中包括三个调用场景:

1、元素为空,第一次添加元素时;

   if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

只有实际添加元素时,才真正得分配空间。

2、元素数量超过阈值时;

    if (++size > threshold)
            resize();

        代码中的阈值threshold是根据容量*负载因子算出来的。如默认容量是16,负载因子是0.75,则阈值为16*0.75=12。这里你可能要问了,为啥负载因子时0.75,默认值为啥是,这个是根据经验得到的阈值。

3、进行红黑树转换时:

   final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
    	  ......... 
   }

        这个也是精妙之处,当整个数组容量比较小的时候,如果冲突元素多了,没必要进行红黑树转换,可以直接扩容,这样就可以减少频繁的数据结构转换。MIN_TREEIFY_CAPACITY默认是64.

        说完resize时机,在看看HashMap的具体resize过程:

1、设置新的容量capicaity以及新的阈值,新数组容量是旧的2倍。注意这里的边界问题,旧值以及新值是否超过最大值。HashMap的最大值是:

    static final int MAXIMUM_CAPACITY = 1 << 30;

我们在日常编程中,也要时刻注意边界问题,写流程很简单,但是否可以对所有场景实现全覆盖还是考研功底的,比如基础的边界问题。

2、开始遍历原hash表的数组:

  • 将旧数组对应位置元素置为null,待GC回收;
  • 如果某个索引位置元素没有冲突,直接计算出新数组的索引,将元素放过去;
   if (e.next == null)
       newTab[e.hash & (newCap - 1)] = e;
  • 如果是红黑树,则进行split,目的是缩小红黑树,或者直接转化为链表,这个不细致讨论;
     else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
  • 如果是链表,会将旧数组的整个冲突链表直接分成两部分。一部分放在和旧数组相同的索引上;一部分直接放在原数组索引+旧数组长度的位置上。这个地方设置的比较巧妙。不需要重新计算索引值。就可以得到目标位置。
											  //用了两个链表,分别表示新数组的低位和高位
												Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            //这个意味着进一位的与是0,所以,索引肯定还是原位置
                            if ((e.hash & oldCap) == 0) {
                               //初始化
                                if (loTail == null)
                                    loHead = e;
                                else
                                   //添加到尾部
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                   //添加到尾部
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        //loHead存储的都是原位置的数据,而且保持了原先的顺序
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
										  //高位的数据,新位置肯定是原位置像左位移一位,即加上原数组长度。
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }    

至此,HashMap的resize过程结束了。再放一下resize的全部源码:

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
       //新的容量和阈值的设置
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        //开始遍历旧数组
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                   //没有冲突元素,直接算出索引,放到新数组中
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                  //红黑树的拆分
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    //移动链表
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

三、Redis的rehash

        相比于HashMap,Redis的rehash操作比较特殊,最大的不同是其reheash是渐进式的,不是一次完成的,而是经过多次完成的,每次只处理一定步长的数据。通常称之为渐进式rehash。这样做的原因是rehash本身是一个比较耗时的操作,为了不影响性能,Redis将整个rehash过程分成多次执行。

        这里再多说一嘴,JAVA的ConcurrentHashMap同样也是渐进式的,其是通过多线程共同协助rehash完成的,和Redis的渐进式本质是一个目的,但实现方式完全不一样。

        此外,Redis注解准备了两个数组,h[1]基本上是h[0]的两倍。rehash的过程简单说就是将h[0]的数据移动到h[1]中。示意图来自于小林coding。

具体流程:

1、为h[1]分配空间, 大小为h[0]容量的2倍;

          return dictExpand(d, d->ht[0].used*2);

2、进行n步的rehash操作,并自增rehash索引值,rehashidx,用来说明当前正在进行rehash,没进行一次rehash步骤,该rehashidx则加1;

3、每次在进行增删改查操作时,如果rehashidx大于等于0,则表示rehash正在进行中,就会首先进行一次rehash过程;

4、经过多次过程,rehash全部执行完成,rehashidx会被设置为-1,表示已完成。

注意Redis采用链表头插法解决冲突的,经过rehash操作后,如果还在新数组的同一索引位置上,可能会导致顺序发生变化。

看一下源码:

int dictRehash(dict *d, int n) {
    //rehashidx最多连续移动 10倍的长度,
    int empty_visits = n*10; /* Max number of empty buckets to visit. */
    if (!dictIsRehashing(d)) return 0;

   //循环遍历ht[0],执行n步骤,且不为空,
    while(n-- && d->ht[0].used != 0) {
        dictEntry *de, *nextde;
        //直到获取第一个rehash值不为空
        while(d->ht[0].table[d->rehashidx] == NULL) {
            d->rehashidx++;
            if (--empty_visits == 0) return 1;
        }
        //获取到对应的dictEntry
        de = d->ht[0].table[d->rehashidx];
        /* Move all the keys in this bucket from the old to the new hash HT */
        //这一个bucket就是一个链表,在同一个索引位置上的元素,一次rehash步骤会一起移过去。
        while(de) {
            uint64_t h;

            nextde = de->next;
            //新的hash表中,链表顺序可能发生了变化
            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++;
    }

    /* 如果整个h[0]都被移动完成,则将h[1]赋值给h[0],h[1]中的空间被重置。
    if (d->ht[0].used == 0) {
        zfree(d->ht[0].table);
        d->ht[0] = d->ht[1];
        _dictReset(&d->ht[1]);
        d->rehashidx = -1;
        return 0;
    }

    /* More to rehash... */
    return 1;
}

        这里你可能会有个疑问, 如果我们在增删改查的过程中,正在rehash,目前有两个数组,ht[0],ht[1],我该用哪个。实际上,这还要看具体是什么操作。

  • 如果是add操作,则直接放到新数组h[1]中
       ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
  • 如果是查询操作,先遍历ht[0],再遍历ht[1];
   for (table = 0; table <= 1; table++) {
        idx = h & d->ht[table].sizemask;
        he = d->ht[table].table[idx];
        while(he) {
            if (key==he->key || dictCompareKeys(d, key, he->key))
                return he;
            he = he->next;
        }
        if (!dictIsRehashing(d)) return NULL;
    }
  • 如果是删除操作,和查询一样,遍历两个数组,执行相同的删除操作。

Rehash触发时机:

        什么时候触发rehash呢?HashMap中定义了装载因子,默认值是0.75(元素个数/总容量),装载因子设置过大,就非常有可能引起冲突,导致单个索引下出现链表或者红黑数结构,不利于查找;装载因子过小,可能会造成空间的浪费。

然而Redis的装载因子设置为1,此外在判断是不是需要rehash的规则是:

1、服务器目前没有在执行 BGSAVE/BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 1;

2、服务器目前正在执行 BGSAVE/BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 5;

源码:

/* Expand the hash table if needed */
static int _dictExpandIfNeeded(dict *d)
{
    /* Incremental rehashing already in progress. Return. */
    // 如果正在进行渐进式扩容,则返回OK
    if (dictIsRehashing(d)) return DICT_OK;

    /* If the hash table is empty expand it to the initial size. */
    // 如果哈希表ht[0]的大小为0,则初始化字典
    if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);

    /* If we reached the 1:1 ratio, and we are allowed to resize the hash
     * table (global setting) or we should avoid it but the ratio between
     * elements/buckets is over the "safe" threshold, we resize doubling
     * the number of buckets. */
    /*
     * 如果哈希表ht[0]中保存的key个数与哈希表大小的比例已经达到1:1,即保存的节点数已经大于哈希表大小
     * 且redis服务当前允许执行rehash,或者保存的节点数与哈希表大小的比例超过了安全阈值(默认值为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].used*2);
    }
    return DICT_OK;
}

        之所以设置大于等于1,首先是Redis不希望经常进行rehash操作,因为rehash本身也是耗时的,此外Redis有过期淘汰策略,会惰性或者定时删除一些key,没必要总是执行rehash操作。

至此,介绍完了哈希表的数据结构,扩容机制,本文结束!

参考资料:

千与千寻-Redis数据结构

Redis原理再学习04:数据结构-哈希表hash表(dict字典) - 九卷 - 博客园

Redis hash实现详解 - 简书

Redis的渐进式rehash原理 - 知乎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值