Redis rehash

Redis整体上是以KV形式存储的,V可以有几种类型:string、hash、list、set、zset。

KV存储对应的数据结构类似HashMap:数组+链表。

这种数据结构的理想状态就是所有的key均衡的分布在数组的每个槽位上,链表长度尽可能的短。

定义一个负载因子loadFactory:KV的数量/数组长度。

当loadFactor>1时,必定有至少1个槽位上的链表长度超过1,对于链表的查询时间复杂度为O(n),n是链表长度。

当loadFactor超过一定数值时,说明链表长度也会比较长,此时查询时间会变长,因此需要进行扩容数组长度,然后把已经保存的KV重新hash到新的更大的数组上,即rehash。

源码:

struct redisServer {
...    
    redisDb *db;
...
}
typedef struct redisDb {
    dict *dict;                 /* The keyspace for this DB */
...
} redisDb;
typedef struct dict {
...
    dictht ht[2];
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
...
} dict;
typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;
typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;

对应关系:

一个redisServer可以对应多个redisDb(默认16个),但是通常只是用其中一个,即第0个。

一个redisDb对应一个dict,这里是全局范围KV对应的dict,如果V是hash类型的值,那么一个KV对应一个dict。

一个dict对应2个dictht,但是正常情况只用其中一个,只有在做rehash时,会使用另一个作为rehash的目的地,rehash结束后恢复使用其中一个。

dictht.table即是KV存储结构中的数组,数组的每一个槽位dictEntry及dictEntry->next就构成了链表。


一般情况,rehash会由新设置key的时候触发开始。

但是因为redis是单线程,所以如果K很多时,不可能一步完成整个rehash操作,所以需要渐进式rehash。

分为两种:

1)操作redis时,额外做一步rehash

对redis做读取、插入、删除等操作时,会额外把位于table[dict->rehashidx]位置的链表移动到新的dictht中,然后把rehashidx做加一操作,移动到后面一个槽位。

2)后台定时任务调用rehash

后台定时任务rehash调用链

serverCron() - databasesCron() - incrementallyRehash() - dictRehashMilliseconds() - dictRehash()

控制rehash调用频率:每秒钟调用10次serverCron()方法,由server.hz配置。


rehash源码:

// n:最多做几个槽位对应链表的rehash
int dictRehash(dict *d, int n) {
    // 如果连续遍历到10*n个空的槽位,就结束这一步rehash,等下一步调用到。
    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;
        }
		// 找到一个非空的槽位,把对应的链表中每一个node都转移到新的ht中
        de = d->ht[0].table[d->rehashidx];
        /* Move all the keys in this bucket from the old to the new hash HT */
        while(de) {
            unsigned int h;

            nextde = de->next;
            /* Get the index in the new hash table */
			// 计算在新的ht中的数组位置
            h = dictHashKey(d, de->key) & d->ht[1].sizemask;
			// 插到链表的头部
            de->next = d->ht[1].table[h];
            d->ht[1].table[h] = de;
			// 把老的ht容量减一,新的ht容量加一
            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... */
	// 如果老的ht已经是空的了,把老的内存回收掉,然后把新老ht对调,仍然继续使用老的ht
    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;
}

在redis做rehash的过程中,因为存在两个数组+链表结构,所以从redis查询、删除、更新数据时,要先到老的ht操作,如果不存在再到新的ht操作;新插入的直接插入到新的ht即可。

你可能感兴趣的:(Redis)