Redis源码阅读之渐进式rehash

Redis源码阅读之渐进式rehash

June 22, 2024
Redis
Redis
Redis版本:7.0.2

在哈希表扩容或缩容时,如果一次迁移完所有数据,可能会导致性能阻塞问题。为了避免出现这种情况,Redis采用了渐进式 rehash的方式,将rehash操作分为多次进行,以确保系统的稳定性。在rehash过程中,redis会逐步将旧哈希表中的数据迁移到新 哈希表中,直到旧哈希表中的所有数据都迁移完毕。

实际上,不是只有hash类型会用到dict,list、set、zset等类型也会用到dict。而每个database中也会有一个全局字典和过期字典。

我们今天就来阅读一下redis中渐进式rehash的实现。

dict-Redis中的字典类型定义 #

在redis中,字典(dict)类型的定义如下:

/*****************dict.h********************/
struct dict {
    dictType *type;

    dictEntry **ht_table[2];
    unsigned long ht_used[2];

    long rehashidx; /* rehashing not in progress if rehashidx == -1 */

    /* Keep small vars at end for optimal (minimal) struct padding */
    int16_t pauserehash; /* If >0 rehashing is paused (<0 indicates coding error) */
    signed char ht_size_exp[2]; /* exponent of size. (size = 1<<exp) */
};

其中字段介绍如下:

  • type:字典类型
  • ht_table:哈希表数组,ht_table[0]为旧哈希表,ht_table[1]为新哈希表。添加喜暖苏时,如果正在rehash,会写入ht_table[1],否则会写入ht_table[1]。
  • ht_used:哈希表已使用的槽位数
  • rehashidx:当前正在迁移的槽位索引,当rehashidx为-1时,表示没有rehash操作正在进行红。。
  • pauserehash:是否暂停rehash操作。
  • ht_size_exp:哈希表大小的指数值。

dictRehash #

/*******************************dict.c*********************************/
/* Performs N steps of incremental rehashing. Returns 1 if there are still
 * keys to move from the old to the new hash table, otherwise 0 is returned.
 * 执行N次增量rehash。如果在旧hash表中仍然有需要移动的键,返回1,否则返回0.
 *
 * Note that a rehashing step consists in moving a bucket (that may have more
 * than one key as we use chaining) from the old to the new hash table, however
 * since part of the hash table may be composed of empty spaces, it is not
 * guaranteed that this function will rehash even a single bucket, since it
 * will visit at max N*10 empty buckets in total, otherwise the amount of
 * work it does would be unbound and the function may block for a long time. 
 * 请注意,rehashing的一步操作将一个bucket(应为我们用了链表,所以实际上可能会有多个key)
 * 从旧的哈希表移动到新的哈希表。然而,由于哈希表的部分可能是空白空间,因此不能保证这个函数
 * 一定会rehash一个桶。它最多会访问N*10个空桶,否则它的工作量将是无限的,函数可能会阻塞很长时间。
 * */
int dictRehash(dict *d, int n)
{
    int empty_visits = n * 10; /* Max number of empty buckets to visit. */
    if (!dictIsRehashing(d))
        return 0;

    while (n-- && d->ht_used[0] != 0)
    {
        dictEntry *de, *nextde;

        /* Note that rehashidx can't overflow as we are sure there are more
         * elements because ht[0].used != 0 */
        assert(DICTHT_SIZE(d->ht_size_exp[0]) > (unsigned long)d->rehashidx);
        while (d->ht_table[0][d->rehashidx] == NULL)
        {
            d->rehashidx++;
            if (--empty_visits == 0)
                return 1;
        }
        de = d->ht_table[0][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) & DICTHT_SIZE_MASK(d->ht_size_exp[1]);
            de->next = d->ht_table[1][h];
            d->ht_table[1][h] = de;
            d->ht_used[0]--;
            d->ht_used[1]++;
            de = nextde;
        }
        d->ht_table[0][d->rehashidx] = NULL;
        d->rehashidx++;
    }

    /* Check if we already rehashed the whole table... */
    if (d->ht_used[0] == 0)
    {
        zfree(d->ht_table[0]);
        /* Copy the new ht onto the old one */
        d->ht_table[0] = d->ht_table[1];
        d->ht_used[0] = d->ht_used[1];
        d->ht_size_exp[0] = d->ht_size_exp[1];
        _dictReset(d, 1);
        d->rehashidx = -1;
        return 0;
    }

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

逻辑为:

  1. 校验rehash状态。
  2. 遍历旧hash表ht_table[0],从下标rehashidx开始,找到第一个非空桶de。

此处请注意,有一个整型变量empty_visits = n*10,用于限制最大访问空桶的次数,如果遍历ht_table[0]十次还没有找到非空桶,则会直接中断短讯,返回1,避免rehash操作阻塞太久。

  1. 遍历de链表,将每个元素移动到新hash表ht_table[1]中。
  2. 如果旧hash表ht_table[0]中的桶移动全部移动到了新桶中,则释放旧hash表内存,将新hash表赋值给旧hash表,重置rehash状态,返回0。
  3. 如果还有未迁移的桶,则返回1。

小结 #

  • 字典dict的哈希表(ht_table)是一个二维数组,我们可以认为ht_table[0]为旧哈希表,ht_table[1]为新哈希表。
  • 每次添加元素时,如果正在进行rehash,添加到ht_table[1]中,否则,添加到ht_table[0]中。
  • Redis的哈希表用的是拉链法,即数组+链表的方式,每个槽位上的元素是一个链表。
  • 每次迁移时,实际上不是只迁移一个key,而是将这个桶(或者说槽)的链表上的所有key都迁移到新哈希表中。
  • 为了避免rehash操作阻塞太久,每次rehash操作最多只会访问10个空桶。