redis什么时候执行rehash

https://blog.csdn.net/Oooo_mumuxi/article/details/105903889

前言
上一章把Redis基础类型介绍完了,更深的问题便会问:哈希表会有什么缺点?或者你了解hash吗?它是怎么解决冲突的?Redis渐进式rehash的原理是什么?
下面就来深入的解析这些问题。

一、字典
字典是Redis中存在最广泛的一种数据结构不仅在哈希对象,集合对象和有序结合对象中都有使用,而且Redis所有的Key,Value都是存在db->dict这张字典中的。

Redis 的字典使用哈希表作为底层实现。

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];   // 包含一堆 dictEntry 即下面的结构体
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    unsigned long iterators; /* number of iterators currently running */
} dict;


typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    // Redis 的哈希表使用链地址法(separate chaining)来解决键冲突:
    // 每个哈希表节点都有一个 next 指针, 多个哈希表节点可以用 next 指针构成一个单向链表,
    // 被分配到同一个索引上的多个节点可以用这个单向链表连接起来, 这就解决了键冲突的问题。
    struct dictEntry *next;
} dictEntry;

// 这2个结构体要好好记住理解。

rehash操作
rehash解决了什么问题:通过扩容解决hash冲突,解决字典的性能问题。

扩容操作

哈希表的负载因子(used/size)
a. 如果没有进行bgsave 元素数量达到hash长度时就会扩容(负载因子大于等于 1)
b. 如果进行bgsave,元素数量达到hash长度的5倍会进行扩容(负载因子大于等于 5)
收缩操作

当哈希表的负载因子小于 0.1时, 程序自动开始对哈希表执行收缩操作。

负载因子是个动态值,满足一定条件的时候会触发rehash,rehash包括扩容和缩容

rehash步骤:

在字典中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 , 表示 rehash 工作正式开始;
为 ht[1] 分配空间, 让字典同时持有 ht[0] 和 ht[1] 两个哈希表
若是扩展操作,那么ht[1]的大小为>=ht[0].used*2的2^n
若是收缩操作,那么ht[1]的大小为>=ht[0].used的2^n

将保存在 ht[0] 中的所有键值对 rehash 到 ht[1] 上
当 ht[0] 包含的所有键值对全部迁移到了 ht[1] 之后,释放 ht[0] ,将 ht[1] 设置为 ht[0],并重置 ht[1] ,最好将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。
rehashidx还有一个作用:记录的是ht[0]的下标位置的位置,下次rehash就从该位置继续进行。

//部分代码
if (d->ht[0].used == 0) {
   // 释放ht[0]哈希表
    zfree(d->ht[0].table);
    // 将原来的ht[1]号哈希表设置为新的ht[0]哈希表
    d->ht[0] = d->ht[1];
    // 重置旧的ht[1]哈希表
    _dictReset(&d->ht[1]);
    // 关闭 rehash 标识
    d->rehashidx = -1;
    // 返回 0 ,向调用者表示 rehash 已经完成
    return 0;
}

那Redis到底在什么时候去判断负载因子是否满足条件然后执行rehash?

/* This function handles 'background' operations we are required to do
 * incrementally in Redis databases, such as active key expiring, resizing,
 * rehashing. */
void databasesCron(void) {
    ...
    //省略部分代码
    ...
    /* Rehash */
    // 对字典进行渐进式 rehash
    if (server.activerehashing) {
        for (j = 0; j < dbs_per_call; j++) {
            int work_done = incrementallyRehash(rehash_db % server.dbnum);
            rehash_db++;
            if (work_done) {
                /* If the function did some work, stop here, we'll do
                 * more at the next cron loop. */
                break;
            }
        }
    }
}

int incrementallyRehash(int dbid) {
    /* Keys dictionary */
    if (dictIsRehashing(server.db[dbid].dict)) {
        dictRehashMilliseconds(server.db[dbid].dict,1);
        return 1; /* already used our millisecond for this loop... */
    }
    /* Expires */
    if (dictIsRehashing(server.db[dbid].expires)) {
        dictRehashMilliseconds(server.db[dbid].expires,1);
        return 1; /* already used our millisecond for this loop... */
    }
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
/* Rehash for an amount of time between ms milliseconds and ms+1 milliseconds */
int dictRehashMilliseconds(dict *d, int ms) {
    long long start = timeInMilliseconds();
    int rehashes = 0;
    while(dictRehash(d,100)) {
        rehashes += 100;
        if (timeInMilliseconds()-start > ms) break;
    }
    return rehashes;
}

// 最终执行的函数
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[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++;
    }

    /* Check if we already rehashed the whole table... */
    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;
}

总结一下:
入口是 server.c文件下的serverCron函数,该函数是RedisServer每秒执行一次。
serverCron函数异步执行的操作:

Active expired keys collection (it is also performed in a lazy way on lookup).
Software watchdog.
Update some statistic.
Incremental rehashing of the DBs hash tables.
Triggering BGSAVE / AOF rewrite, and handling of terminated children.
Clients timeout of different kinds.
Replication reconnection.
Many more…
serverCron函数会调用: databasesCron() -> incrementallyRehash() -> dictRehashMilliseconds() -> dictRehash()。

serverCron是每秒执行,但dictRehash是每100ms里面使用1ms时间执行一次,每次执行的steps为100。
So we try to use 1 millisecond of CPU time at every call of this function to perform some rehahsing。
Move all the keys in this bucket from the old to the new hash HT。
Active rehashing uses 1 millisecond every 100 milliseconds of CPU time。

上面说的是Active rehashing,Redis还有一个操作也会触发dictRehash()。

lazy rehashing:在每次对dict进行操作的时候执行一次dictRehash,steps为1。

/* This function performs just a step of rehashing, and only if there are
 * no safe iterators bound to our hash table. When we have iterators in the
 * middle of a rehashing we can't mess with the two hash tables otherwise
 * some element can be missed or duplicated.
 *
 * This function is called by common lookup or update operations in the
 * dictionary so that the hash table automatically migrates from H1 to H2
 * while it is actively used. */
static void _dictRehashStep(dict *d) {
    if (d->iterators == 0) dictRehash(d,1);
}

_dictRehashStep是被dictAddRaw、dictGenericDelete、dictFind、dictGetRandomKey、dictGetSomeKeys调用的,因此在每次dict增删查改时都会被调用,加快了rehash过程。

总结
Redis为了保证hash表的效率,会通过判断负载因子的情况来对hash表进行扩容或缩容。因为数据容量越来越多的情况下hash碰撞的概率会越来越高,因此通过扩容,实际就是rehash操作来解决hash碰撞问题。

上一篇:PHP底层数据存储结构-哈希表


下一篇:青龙面板运行欢太