Redis的Hash的实现

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: Redis的Hash的实现

我们知道,Hash 表是一种非常关键的数据结构,在计算机系统中发挥着重要作用。

比如在 Memcached 中,Hash 表被用来索引数据;在数据库系统中,Hash 表被用来辅助 SQL 查询。而对于 Redis 键值数据库来说,Hash 表既是键值对中的一种值类型,同时,Redis 也使用一个全局 Hash 表来保存所有的键值对,从而既满足应用存取 Hash 结构数据需求,又能提供快速查询功能。 那么,Hash 表应用如此广泛的一个重要原因,就是从理论上来说,它能以 O(1) 的复杂度 快速查询数据。Hash 表通过 Hash 函数的计算,就能定位数据在表中的位置,紧接着可以 对数据进行操作,这就使得数据操作非常快速。

Hash 表这个结构也并不难理解,但是在实际应用 Hash 表时,当数据量不断增加,它的性能就经常会受到哈希冲突和 rehash 开销的影响。而这两个问题的核心,其实都来自于 Hash 表要保存的数据量,超过了当前 Hash 表能容纳的数据量。 那么要如何应对这两个问题呢?事实上,这也是在大厂面试中,面试官经常会考核的问题。所以你现在可以先想想,如果你在面试中遇到了这两个问题,你会怎么回答呢? OK,思考先到这里,现在我来告诉你 Redis 是怎么很好地解决这两个问题的。

Redis 为我们提供了一个经典的 Hash 表实现方案。针对哈希冲突,Redis 采用了链式哈希,在不扩容哈希表的前提下,将具有相同哈希值的数据链接起来,以便这些数据在表中 仍然可以被查询到;对于 rehash 开销,Redis 实现了渐进式 rehash 设计,进而缓解了 rehash 操作带来的额外开销对系统的性能影响。

Redis 如何实现链式哈希?

不过,在开始学习链式哈希的设计实现之前,我们还需要明白 Redis 中 Hash 表的结构设 计是啥样的,以及为何会在数据量增加时产生哈希冲突,这样也更容易帮助我们理解链式 哈希应对哈希冲突的解决思路。

什么是哈希冲突?

实际上,一个最简单的 Hash 表就是一个数组,数组里的每个元素是一个哈希桶(也叫做 Bucket),第一个数组元素被编为哈希桶 0,以此类推。当一个键值对的键经过 Hash 函 数计算后,再对数组元素个数取模,就能得到该键值对对应的数组元素位置,也就是第几个哈希桶。

如下图所示,key1 经过哈希计算和哈希值取模后,就对应哈希桶 1,类似的,key3 和 key16 分别对应哈希桶 7 和桶 4。

从图上我们还可以看到,需要写入 Hash 表的键空间一共有 16 个键,而 Hash 表的空间大小只有 11 个元素,这样就会导致有些键会对应到相同的哈希桶中。

我们在实际应用 Hash 表时,其实一般很难预估要保存的数据量,如果我们一开始就创建 一个非常大的哈希表,当数据量较小时,就会造成空间浪费。所以,我们通常会给哈希表 设定一个初始大小,而当数据量增加时,键空间的大小就会大于 Hash 表空间大小了。

也正是由于键空间会大于 Hash 表空间,这就导致在用 Hash 函数把键映射到 Hash 表空 间时,不可避免地会出现不同的键被映射到数组的同一个位置上。而如果同一个位置只能 保存一个键值对,就会导致 Hash 表保存的数据非常有限,这就是我们常说的哈希冲突

比如下图中,key4 和 key5 都被映射到了 Hash 表的桶 10 中,这样,当桶 10 只能保存 一个 key 时,key4 和 key5 就会有一个 key 无法保存到哈希表中了

那么我们该如何解决哈希冲突呢?可以考虑使用以下两种解决方案:

  • 第一种方案,就是我接下来要给你介绍的链式哈希。这里你需要先知道,链式哈希的链 不能太长,否则会降低 Hash 表性能。
  • 第二种方案,就是当链式哈希的链长达到一定长度时,我们可以使用 rehash。不过, 执行 rehash 本身开销比较大,所以就需要采用我稍后会给你介绍的渐进式 rehash 设 计。

这里,我们先来了解链式哈希的设计和实现。

链式哈希如何设计与实现? 所谓的链式哈希,就是用一个链表把映射到 Hash 表同一桶中的键给连接起来。下面我们 就来看看 Redis 是如何实现链式哈希的,以及为何链式哈希能够帮助解决哈希冲突。

首先,我们需要了解 Redis 源码中对 Hash 表的实现。Redis 中和 Hash 表实现相关的文 件主要是 dict.h 和 dict.c。其中,dict.h 文件定义了 Hash 表的结构、哈希项,以及 Hash 表的各种操作函数,而 dict.c 文件包含了 Hash 表各种操作的具体实现代码。 在 dict.h 文件中,Hash 表被定义为一个二维数组(dictEntry **table),这个数组的每个 元素是一个指向哈希项(dictEntry)的指针。下面的代码展示的就是在 dict.h 文件中对 Hash 表的定义,你可以看下:

/* This is our hash table structure. Every dictionary has two of this as we
 * implement incremental rehashing, for the old to the new table. */
typedef struct dictht {
    // 二维数组
    dictEntry **table;
    // Hash表大小
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;

那么为了实现链式哈希, Redis 在每个 dictEntry 的结构设计中,除了包含指向键和值的指针,还包含了指向下一个哈希项的指针。如下面的代码所示,dictEntry 结构体中包含了 指向另一个 dictEntry 结构的指针 *next,这就是用来实现链式哈希的:

typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;

除了用于实现链式哈希的指针外,这里还有一个值得注意的地方,就是在 dictEntry 结构体 中,键值对的值是由一个联合体 v 定义的。这个联合体 v 中包含了指向实际值的指针 *val,还包含了无符号的 64 位整数、有符号的 64 位整数,以及 double 类的值。

我之所以要提醒你注意这里,其实是为了说明,这种实现方法是一种节省内存的开发小技 巧,非常值得学习。因为当值为整数或双精度浮点数时,由于其本身就是 64 位,就可以不 用指针指向了,而是可以直接存在键值对的结构体中,这样就避免了再用一个指针,从而 节省了内存空间。 好了,那么到这里,你应该就了解了 Redis 中链式哈希的实现,不过现在你可能还是不太 明白,为什么这种链式哈希可以帮助解决哈希冲突呢? 别着急,我就拿刚才的例子来说明一下,key4 和 key5 都被映射到了 Hash 表的桶 9 中。而当使用了链式哈希,桶 9 就不会只保存 key4 或 key5,而是会用一个链表把key4 和 key5 连接起来,如下图所示。当有更多的 key 被映射到桶 9 时,这些 key 都 可以用链表串接起来,以应对哈希冲突。

这样,当我们要查询 key5时,可以先通过哈希函数计算,得到 key5的哈希值被映 射到了桶 9 中。然后,我们再逐一比较桶 9 中串接的 key,直到查找到 key5。如此一 来,我们就能在链式哈希中找到所查的哈希项了。 不过,链式哈希也存在局限性,那就是随着链表长度的增加,Hash 表在一个位置上查询哈 希项的耗时就会增加,从而增加了 Hash 表的整体查询时间,这样也会导致 Hash 表的性 能下降。

那么,有没有什么其他的方法可以减少对 Hash 表性能的影响呢?当然是有的,这就是接 下来我要给你介绍的 rehash 的设计与实现了。

Redis 如何实现 rehash?

rehash 操作,其实就是指扩大 Hash 表空间。而 Redis 实现 rehash 的基本思路是这样 的:

首先,Redis 准备了两个哈希表,用于 rehash 时交替保存数据。我在前面给你介绍过,Redis 在 dict.h 文件中使用 dictht 结构体定义了 Hash 表。不过, 在实际使用 Hash 表时,Redis 又在 dict.h 文件中,定义了一个 dict 结构体。这个结构体中有一个数组(ht[2]),包含了两个 Hash 表 ht[0]和 ht[1]。dict 结构体的代码定义如下 所示:

typedef struct dict {
    dictType *type;
    void *privdata;
    //两个Hash表,交替使用,用于rehash操作
    dictht ht[2];
    // Hash表是否在进行rehash的标识,-1表示没有进行rehash
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    int16_t pauserehash; /* If >0 rehashing is paused (<0 indicates coding error) */
} dict;

其次,在正常服务请求阶段,所有的键值对写入哈希表 ht[0]。

接着,当进行 rehash 时,键值对被迁移到哈希表 ht[1]中。

最后,当迁移完成后,ht[0]的空间会被释放,并把 ht[1]的地址赋值给 ht[0],ht[1]的表 大小设置为 0。这样一来,又回到了正常服务请求的阶段,ht[0]接收和服务请求,ht[1] 作为下一次 rehash 时的迁移表。

好,那么在了解了 Redis 交替使用两个 Hash 表实现 rehash 的基本思路后,我们还需要 明确的是:在实现 rehash 时,都需要解决哪些问题?我认为主要有以下三点:

  • 什么时候触发 rehash?
  • rehash 扩容扩多大?
  • rehash 如何执行?

所以下面,我就带你来逐一学习 Redis 对这三个问题的代码实现,通过代码实现,你就能 明晰 Redis 针对这三个问题的设计思想了。

什么时候触发 rehash?

首先要知道,Redis 用来判断是否触发 rehash 的函数是 _dictExpandIfNeeded。所以接 下来我们就先看看, _dictExpandIfNeeded函数中进行扩容的触发条件;然后,我们再来 了解下 _dictExpandIfNeeded又是在哪些函数中被调用的。

实际上, _dictExpandIfNeeded函数中定义了三个扩容条件。 下面的代码就展示了 _dictExpandIfNeeded函数对这三个条件的定义,你可以看下。 那么,对于条件一来说,此时 Hash 表是空的,所以 Redis 就需要将 Hash 表空间设置为 初始大小,而这是初始化的工作,并不属于 rehash 操作。

什么时候触发 rehash? rehash 扩容扩多大? rehash 如何执行?

  • 条件一:ht[0]的大小为 0。
  • 条件二:ht[0]承载的元素个数已经超过了 ht[0]的大小,同时 Hash 表可以进行扩容。
  • 条件三:ht[0]承载的元素个数,是 ht[0]的大小的 dict_force_resize_ratio 倍,其中, dict_force_resize_ratio 的默认值是 5。

/* Expand the hash table if needed */
static int _dictExpandIfNeeded(dict *d)
{
    /* Incremental rehashing already in progress. Return. */
    if (dictIsRehashing(d)) return DICT_OK;
    /* If the hash table is empty expand it to the initial size. */
    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]表使用的元素个数超过当前大小
    // 并且可以扩容或者 ht[0]使用的元素个数/ht[0]表的大小 大于 dict_force_resize_ratio
    // 并且能够允许扩展
    if (d->ht[0].used >= d->ht[0].size &&
        (dict_can_resize ||
         d->ht[0].used/d->ht[0].size > dict_force_resize_ratio) &&
        dictTypeExpandAllowed(d))
    {
        return dictExpand(d, d->ht[0].used + 1);
    }
    return DICT_OK;
}

那么,对于条件一来说,此时 Hash 表是空的,所以 Redis 就需要将 Hash 表空间设置为 初始大小,而这是初始化的工作,并不属于 rehash 操作。

而条件二和三就对应了 rehash 的场景。因为在这两个条件中,都比较了 Hash 表当前承载 的元素个数(d->ht[0].used)和 Hash 表当前设定的大小(d->ht[0].size),这两个值的 比值一般称为负载因子(load factor)。也就是说,Redis 判断是否进行 rehash 的条 件,就是看 load factor 是否大于等于 1 和是否大于 5。

实际上,当 load factor 大于 5 时,就表明 Hash 表已经过载比较严重了,需要立刻进行库扩容。而当 load factor 大于等于 1 时,Redis 还会再判断 dict_can_resize 这个变量值,查看当前是否可以进行扩容。 你可能要问了,这里的 dict_can_resize 变量值是啥呀?其实,这个变量值是在 dictEnableResize 和 dictDisableResize 两个函数中设置的,它们的作用分别是启用和禁止哈希表执行 rehash 功能,如下所示:

void dictEnableResize(void) {
    dict_can_resize = 1;
}
void dictDisableResize(void) {
    dict_can_resize = 0;
}

// 下面这一部分在redis6.2.6中并未看到,只是网上有人说这个在redis3.2版本中有看到

然后,这两个函数又被封装在了 updateDictResizePolicy 函数中。

updateDictResizePolicy 函数是用来启用或禁用 rehash 扩容功能的,这个函数调用 dictEnableResize 函数启用扩容功能的条件是:

  • 当前没有 RDB 子进程,并且也没有 AOF 子进程。

这就对应了 Redis 没有执行 RDB 快照和没有进行 AOF 重写的场景。你可以参考下面给出的代码:

void updateDictResizePolicy(void) {
if (server.rdb_child_pid == -1 && server.aof_child_pid == -1)
  dictEnableResize();
else
  dictDisableResize();
}

好,到这里我们就了解了 _dictExpandIfNeeded 对 rehash 的判断触发条件,那么现在, 我们再来看下 Redis 会在哪些函数中,调用 _dictExpandIfNeeded 进行判断。 首先,通过在dict.c文件中查看 _dictExpandIfNeeded 的被调用关系,我们可以发现, _dictExpandIfNeeded 是被 _dictKeyIndex 函数调用的,而 _dictKeyIndex 函数又会被 dictAddRaw 函数调用,然后 dictAddRaw 会被以下三个函数调用。

  • dictAdd:用来往 Hash 表中添加一个键值对。
  • dictRelace:用来往 Hash 表中添加一个键值对,或者键值对存在时,修改键值对。
  • dictAddorFind:直接调用 dictAddRaw

因此,当我们往 Redis 中写入新的键值对或是修改键值对时,Redis 都会判断下是否需要 进行 rehash。这里你可以参考下面给出的示意图,其中就展示了 _dictExpandIfNeeded 被调用的关系。

好了,简而言之,Redis 中触发 rehash 操作的关键,就是**_dictExpandIfNeeded 函数和 updateDictResizePolicy 函数**。_dictExpandIfNeeded 函数会根据 Hash 表的负载因子 以及能否进行 rehash 的标识,判断是否进行 rehash,而 updateDictResizePolicy 函数 会根据 RDB 和 AOF 的执行情况,启用或禁用 rehash。

接下来,我们继续探讨 Redis 在实现 rehash 时,要解决的第二个问题:rehash 扩容扩多 大?

rehash 扩容扩多大?

在 Redis 中,rehash 对 Hash 表空间的扩容是通过调用 dictExpand 函数来完成的。 dictExpand 函数的参数有两个,一个是要扩容的 Hash 表,另一个是要扩到的容量,下面 的代码就展示了 dictExpand 函数的原型定义:

int dictExpand(dict *d, unsigned long size);

那么,对于一个 Hash 表来说,我们就可以根据前面提到的 _dictExpandIfNeeded 函数, 来判断是否要对其进行扩容。而一旦判断要扩容,Redis 在执行 rehash 操作时,对 Hash 表扩容的思路也很简单,就是如果当前表的已用空间大小为 size,那么就将表扩容到 size*2 的大小。

如下所示,当 _dictExpandIfNeeded 函数在判断了需要进行 rehash 后,就调用 dictExpand 进行扩容。这里你可以看到,rehash 的扩容大小是当前 ht[0]已使用大小的 2 倍。

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

而在 dictExpand 函数中,具体执行是由 _dictNextPower 函数完成的,以下代码显示的 Hash 表扩容的操作,就是从 Hash 表的初始大小(DICT_HT_INITIAL_SIZE),不停地乘 以 2,直到达到目标大小。

static unsigned long _dictNextPower(unsigned long size)
{
    // 哈希表的初始大小
    unsigned long i = DICT_HT_INITIAL_SIZE;
  // 如果要扩容的大小已经超过最大值,则返回最大值加1
    if (size >= LONG_MAX) return LONG_MAX + 1LU;
    // 扩容大小没有超过最大值
    while(1) {
        if (i >= size)
            return i;
        // 每一步扩容都在现有大小基础上乘以2
        i *= 2;
    }
}

好,下面我们再来看看 Redis 要解决的第三个问题,即 rehash 要如何执行?而这个问 题,本质上就是 Redis 要如何实现渐进式 rehash 设计。

渐进式 rehash 如何实现?

那么这里,我们要先搞清楚一个问题,就是为什么要实现渐进式 rehash? 其实这是因为,Hash 表在执行 rehash 时,由于 Hash 表空间扩大,原本映射到某一位置 的键可能会被映射到一个新的位置上,因此,很多键就需要从原来的位置拷贝到新的位 置。而在键拷贝时,由于 Redis 主线程无法执行其他请求,所以键拷贝会阻塞主线程,这 样就会产生 rehash 开销

而为了降低 rehash 开销,Redis 就提出了渐进式 rehash 的方法。

简单来说,渐进式 rehash 的意思就是 Redis 并不会一次性把当前 Hash 表中的所有键, 都拷贝到新位置,而是会分批拷贝,每次的键拷贝只拷贝 Hash 表中一个 bucket 中的哈 希项。这样一来,每次键拷贝的时长有限,对主线程的影响也就有限了。

那么,渐进式 rehash 在代码层面是如何实现的呢?这里有两个关键函数:dictRehash 和 _dictRehashStep

我们先来看 dictRehash 函数,这个函数实际执行键拷贝,它的输入参数有两个,分别是 全局哈希表(即前面提到的 dict 结构体,包含了 ht[0]和 ht[1])和需要进行键拷贝的桶数 量(bucket 数量)。

dictRehash 函数的整体逻辑包括两部分:

首先,该函数会执行一个循环,根据要进行键拷贝的 bucket 数量 n,依次完成这些 bucket 内部所有键的迁移。当然,如果 ht[0]哈希表中的数据已经都迁移完成了,键拷 贝的循环也会停止执行

其次,在完成了 n 个 bucket 拷贝后,dictRehash 函数的第二部分逻辑,就是判断 ht[0]表中数据是否都已迁移完。如果都迁移完了,那么 ht[0]的空间会被释放。因为 Redis 在处理请求时,代码逻辑中都是使用 ht[0],所以当 rehash 执行完成后,虽然数据都在 ht[1]中了,但 Redis仍然会把 ht[1]赋值给ht[0],以便其他部分的代码逻辑正常使用。

而在 ht[1]赋值给 ht[0]后,它的大小就会被重置为 0,等待下一次 rehash。与此同时, 全局哈希表中的rehashidx 变量会被标为 -1,表示 rehash 结束了(这里的 rehashidx 变量用来表示 rehash 的进度,稍后我会给你具体解释)。

我画了下面这张图,展示了 dictRehash 的主要执行流程,你可以看下。

int dictRehash(dict *d, int n) {
    int empty_visits = n*10; /* Max number of empty buckets to visit. */
    if (!dictIsRehashing(d)) return 0;
    // 主循环,根据要拷贝的bucket数量n,循环n次后停止或ht[0]中的数据迁移完停止
    while(n-- && d->ht[0].used != 0) {
        dictEntry *de, *nextde;
        /* Note that rehashidx can't overflow as we are sure there are more
         * elements because ht[0].used != 0 */
        assert(d->ht[0].size > (unsigned long)d->rehashidx);
        while(d->ht[0].table[d->rehashidx] == NULL) {
            d->rehashidx++;
            if (--empty_visits == 0) return 1;
        }
        de = d->ht[0].table[d->rehashidx];
        /* Move all the keys in this bucket from the old to the new hash HT */
        while(de) {
            uint64_t h;
            nextde = de->next;
            /* Get the index in the new hash table */
            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++;
    }
    //判断ht[0]的数据是否迁移完成
    /* Check if we already rehashed the whole table... */
    if (d->ht[0].used == 0) {
        // ht[0]迁移完后,释放ht[0]内存空间
        zfree(d->ht[0].table);
        // 让ht[0]指向ht[1],以便接受正常的请求
        d->ht[0] = d->ht[1];
        // 重置ht[1]的大小为0
        _dictReset(&d->ht[1]);
        // 设置全局哈希表的rehashidx标识为-1,表示rehash结束
        d->rehashidx = -1;
        // 返回0,表示ht[0]中所有元素都迁移完
        return 0;
    }
    //返回1,表示ht[0]中仍然有元素没有迁移完
    /* More to rehash... */
    return 1;
}

好,在了解了 dictRehash 函数的主体逻辑后,我们再看下渐进式 rehash 是如何按照 bucket 粒度拷贝数据的,这其实就和全局哈希表 dict 结构中的 rehashidx 变量相关了。

rehashidx 变量表示的是当前 rehash 在对哪个 bucket 做数据迁移。比如,当 rehashidx 等于 0 时,表示对 ht[0]中的第一个 bucket 进行数据迁移;当 rehashidx 等于 1 时,表 示对 ht[0]中的第二个 bucket 进行数据迁移,以此类推。

而 dictRehash 函数的主循环,首先会判断 rehashidx 指向的 bucket 是否为空,如果为 空,那就将 rehashidx 的值加 1,检查下一个 bucket。

那么,有没有可能连续几个 bucket 都为空呢?其实是有可能的,在这种情况下,渐进式 rehash 不会一直递增 rehashidx 进行检查。这是因为一旦执行了 rehash,Redis 主线程 就无法处理其他请求了。

所以,渐进式 rehash 在执行时设置了一个变量 empty_visits,用来表示已经检查过的空 bucket,当检查了一定数量的空 bucket 后,这一轮的 rehash 就停止执行,转而继续处理外来请求,避免了对 Redis 性能的影响。下面的代码显示了这部分逻辑,你可以看下。

// 如果当前要迁移的bucket中没有元素
while(d->ht[0].table[d->rehashidx] == NULL) {
    d->rehashidx++;
    if (--empty_visits == 0) return 1;
}

而如果 rehashidx 指向的 bucket 有数据可以迁移,那么 Redis 就会把这个 bucket 中的 哈希项依次取出来,并根据 ht[1]的表空间大小,重新计算哈希项在 ht[1]中的 bucket 位 置,然后把这个哈希项赋值到 ht[1]对应 bucket 中。

这样,每做完一个哈希项的迁移,ht[0]和 ht[1]用来表示承载哈希项多少的变量 used,就 会分别减一和加一。当然,如果当前 rehashidx 指向的 bucket 中数据都迁移完了, rehashidx 就会递增加 1,指向下一个 bucket。下面的代码显示了这一迁移过程。

while(n-- && d->ht[0].used != 0) {
     dictEntry *de, *nextde;
     /* Note that rehashidx can't overflow as we are sure there are more
         * elements because ht[0].used != 0 */
     assert(d->ht[0].size > (unsigned long)d->rehashidx);
     while(d->ht[0].table[d->rehashidx] == NULL) {
         d->rehashidx++;
         if (--empty_visits == 0) return 1;
     }
     // 获得哈希表中哈希项
     de = d->ht[0].table[d->rehashidx];
     /* Move all the keys in this bucket from the old to the new hash HT */
     while(de) {
         uint64_t h;
     // 获得同一个bucket中下一个哈希项
         nextde = de->next;
         /* Get the index in the new hash table */
         // 根据扩容后的哈希表ht[1]大小,计算当前哈希项在扩容后哈希表中的bucket位置
         h = dictHashKey(d, de->key) & d->ht[1].sizemask;
         // 将当前哈希项添加到扩容后的哈希表ht[1]中
         de->next = d->ht[1].table[h];
         d->ht[1].table[h] = de;
         // 减少当前哈希表的哈希项个数
         d->ht[0].used--;
         // 增加扩容后哈希表的哈希项个数
         d->ht[1].used++;
         de = nextde;
     }
     // 如果当前bucket中已经没有哈希项了,将该bucket置为NULL
     d->ht[0].table[d->rehashidx] = NULL;
     // 将rehash加1,下一次将迁移下一个bucket中的元素
     d->rehashidx++;
 }

好了,到这里,我们就已经基本了解了 dictRehash 函数的全部逻辑。 现在我们知道,dictRehash 函数本身是按照 bucket 粒度执行哈希项迁移的,它内部执行 的 bucket 迁移个数,主要由传入的循环次数变量 n 来决定。但凡 Redis 要进行 rehash操作,最终都会调用 dictRehash 函数。

接下来,我们来学习和渐进式 rehash 相关的第二个关键函数 _dictRehashStep,这个函 数实现了每次只对一个 bucket 执行 rehash。 从 Redis 的源码中我们可以看到,一共会有 5 个函数通过调用 _dictRehashStep 函数,进而调用 dictRehash 函数,来执行 rehash,它们分别是:dictAddRaw, dictGenericDelete,dictFind,dictGetRandomKey,dictGetSomeKeys。

其中,dictAddRaw 和 dictGenericDelete 函数,分别对应了往 Redis 中增加和删除键值对,而后三个函数则对应了在 Redis 中进行查询操作。下图展示了这些函数间的调用关 系:

但你要注意,不管是增删查哪种操作,这 5 个函数调用的 _dictRehashStep 函数,给 dictRehash 传入的循环次数变量 n 的值都为 1,下面的代码就显示了这一传参的情况。

static void _dictRehashStep(dict *d) {
    // 给dictRehash传入的循环次数参数为1,表明每迁移完一个bucket ,就执行正常操作
    if (d->pauserehash == 0) dictRehash(d,1);
}

这样一来,每次迁移完一个 bucket,Hash 表就会执行正常的增删查请求操作,这就是在 代码层面实现渐进式 rehash 的方法。

此文章主要参考极客时间的Redis源码课,增加了一些自己的画图和理解


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
1月前
|
存储 NoSQL Java
Redis如何处理Hash冲突?
在 Redis 中,哈希表是一种常见的数据结构,通常用于存储对象的属性,对于哈希表,最常遇到的是哈希冲突,那么,当 Redis遇到Hash冲突会如何处理?这篇文章,我们将详细介绍Redis如何处理哈希冲突,并探讨其性能和实现细节。
56 1
|
24天前
|
存储 NoSQL Redis
Redis 哈希(Hash)
10月更文挑战第16天
33 1
|
1月前
|
存储 分布式计算 NoSQL
大数据-40 Redis 类型集合 string list set sorted hash 指令列表 执行结果 附截图
大数据-40 Redis 类型集合 string list set sorted hash 指令列表 执行结果 附截图
25 3
|
2月前
|
存储 JSON NoSQL
redis基本数据结构(String,Hash,Set,List,SortedSet)【学习笔记】
这篇文章是关于Redis基本数据结构的学习笔记,包括了String、Hash、Set、List和SortedSet的介绍和常用命令。文章解释了每种数据结构的特点和使用场景,并通过命令示例演示了如何在Redis中操作这些数据结构。此外,还提供了一些练习示例,帮助读者更好地理解和应用这些数据结构。
redis基本数据结构(String,Hash,Set,List,SortedSet)【学习笔记】
|
2月前
|
存储 NoSQL 算法
5)深度解密 Redis 的哈希(Hash)
5)深度解密 Redis 的哈希(Hash)
26 0
|
3月前
|
存储 NoSQL 算法
Redis6入门到实战------ 三、常用五大数据类型(列表(List)、集合(Set)、哈希(Hash)、Zset(sorted set))
这是关于Redis 6入门到实战的文章,具体内容涉及Redis的五大数据类型:列表(List)、集合(Set)、哈希(Hash)、有序集合(Zset(sorted set))。文章详细介绍了这些数据类型的特点、常用命令以及它们背后的数据结构。如果您有任何关于Redis的具体问题或需要进一步的帮助,请随时告诉我。
|
3月前
|
存储 缓存 NoSQL
redis数据结构-hash
redis数据结构-hash
25 0
|
4月前
|
存储 缓存 NoSQL
Redis问题之一致性Hash是如何解决哈希+取余方法中的稳定性问题的
Redis问题之一致性Hash是如何解决哈希+取余方法中的稳定性问题的
65 10
|
4月前
|
消息中间件 JSON NoSQL
Redis深度解析:核心数据类型之hash、list、set
Redis深度解析:核心数据类型之hash、list、set
|
5月前
|
存储 JSON NoSQL
Redis第五弹-HASH结构相关指令和介绍,计数功能Hash-哈希(Redis本来就是键值对结构,哈希,就相当于键值对嵌套了一个键值对)的多种指令Hset key field value-
Redis第五弹-HASH结构相关指令和介绍,计数功能Hash-哈希(Redis本来就是键值对结构,哈希,就相当于键值对嵌套了一个键值对)的多种指令Hset key field value-