redis数据类型-字典-rehash

338 阅读7分钟

redis数据类型-字典-rehash

redis5.8

Can Get

  • 知道什么是 渐进式rehash
  • 渐进式rehash的过程
  • 触发rehash发生的条件
  • 解决hash冲突使用的单链表的头插法
  • rehash的扩容

参考链接

rehash触发(rehashidx=0)

redis用来判断是否触发rehash的函数是 _dictExpandIfNeeded

是否正在rehashing的判断函数

#define dictIsRehashing(d) ((d)->rehashidx != -1)

扩容条件

dict_can_resize = 1

dict_force_resize_ratio=5

loadFactor = d->ht[0].used / d->ht[0].size

  • 条件1,ht[0]的size为0,则默认扩容大小 size=4
  • 条件2,负载因子大于等于1 并且 hash表可以扩容( dict_can_resize)
  • 条件3,负载因子大于5(hash冲突已经很严重,会强制的进行扩容)
  • 条件[2,3]是扩容为 使用节点的2倍
/* Expand the hash table if needed */
static int _dictExpandIfNeeded(dict *d)
{
  	// 字典是否正在reshash中 rehahsidx != -1
    if (dictIsRehashing(d)) return DICT_OK;

  	// ht[0]是空的, 则初始化为size=4
    if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);
  

        // loadFactor = d->ht[0].used / d->ht[0].size
        // (loadFactor >=1 and (dict_can_resize || loadFactor > 5))
        // 负载因子 >=1 and (dict_can_resize and  负载因子 >= 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))
    {
      	// 扩充为 ht[0]节点数量的2倍
        return dictExpand(d, d->ht[0].used*2);
    }
    return DICT_OK;
}

dict_can_resize值的修改

当redis的子进程,正在 rdb快照或者 aof重写时,dict_can_resize=0;会设置禁止改变字典大小,

// dict.h
// 启用
void dictEnableResize(void) {
    dict_can_resize = 1;
}

// 禁用
void dictDisableResize(void) {
    dict_can_resize = 0;
}


// serve.c
void updateDictResizePolicy(void) {
  	// redis没有执行,rdb快照 和 aof重写,则可以扩容
    if (server.rdb_child_pid == -1 && server.aof_child_pid == -1)
        dictEnableResize();
    else
        dictDisableResize();
}

调用关系图

redis的添加和修改都会判断是否进行rehash,调用示意图。

_dictExpandIfNeeded:会根据hash表的负载因子和dict_can_resize 是否能进行rehahs的标识,判断是否进行rehash

updateDictResizePolicy:会根据RDB和AOF的执行情况,启动和禁用rehash

image.png

rehash扩容

dict *d : 要扩容的hash表

unsigned log size :要扩容的容量的大小

以4为基数,每次以2倍的数量增加 直到 大于 size

/* Expand or create the hash table */
int dictExpand(dict *d, unsigned long size)
{
    /* the size is invalid if it is smaller than the number of
     * elements already inside the hash table */
    if (dictIsRehashing(d) || d->ht[0].used > size)
        return DICT_ERR;

    dictht n; /* the new hash table */
  	// 根据size 返回一个realsize
    unsigned long realsize = _dictNextPower(size);

    /* Rehashing to the same table size is not useful. */
    if (realsize == d->ht[0].size) return DICT_ERR;

    /* Allocate the new hash table and initialize all pointers to NULL */
    n.size = realsize;
    n.sizemask = realsize-1; //  size-1
    n.table = zcalloc(realsize*sizeof(dictEntry*));
    n.used = 0;

    /* Is this the first initialization? If so it's not really a rehashing
     * we just set the first hash table so that it can accept keys. */
    if (d->ht[0].table == NULL) {
        d->ht[0] = n;
        return DICT_OK;
    }

    /* Prepare a second hash table for incremental rehashing */
    d->ht[1] = n;
    d->rehashidx = 0; // 注意这里的 rehasidx值为=0 表示rehasing开始了
    return DICT_OK;
}

// 返回 realsize 
static unsigned long _dictNextPower(unsigned long size)
{
    unsigned long i = DICT_HT_INITIAL_SIZE; // 4 

    if (size >= LONG_MAX) return LONG_MAX + 1LU;
    while(1) {
        if (i >= size)
            return i;
        i *= 2;
    }
}

rehash缩容

serverCron() is called periodically (according to server.hz frequency), and performs tasks that must

通过一个redis的后台进程定时任务,定期的去调用 serverCron->databasesCron->tryResizeHashTables,来检查是否需要缩容.

  • tryResizeHashTables
    • 字典的数量 大于 4 并且 字段使用的节点数量低于数组的长度的10%则调用 dictResize
  • dictResize
    • dict_can_resize(rdb没有在快照和aof页没有在重写)并且 字典没有此时没有处于 rehashing状态中。
  • 则开始缩容
// dict.c
int dictResize(dict *d)
{
    int minimal;

    if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;
    minimal = d->ht[0].used;
    if (minimal < DICT_HT_INITIAL_SIZE)
        minimal = DICT_HT_INITIAL_SIZE;
    return dictExpand(d, minimal);
}

#define dictSlots(d) ((d)->ht[0].size+(d)->ht[1].size)
#define dictSize(d) ((d)->ht[0].used+(d)->ht[1].used)
#define DICT_HT_INITIAL_SIZE     4

// server.h
#define HASHTABLE_MIN_FILL        10      /* Minimal hash table fill 10% */


// server.c
/* If the percentage of used slots in the HT reaches HASHTABLE_MIN_FILL
 * we resize the hash table to save memory */
void tryResizeHashTables(int dbid) {
    if (htNeedsResize(server.db[dbid].dict))
        dictResize(server.db[dbid].dict);
    if (htNeedsResize(server.db[dbid].expires))
        dictResize(server.db[dbid].expires);
}

int htNeedsResize(dict *dict) {
    long long size, used;

    size = dictSlots(dict); // ht[0] + ht[1]的size的大小
    used = dictSize(dict); // ht[0] + ht[1]的 used的大小
    return (size > DICT_HT_INITIAL_SIZE && (used*100/size < HASHTABLE_MIN_FILL));
}

rehash实现

hash冲突解决

  • redis解决hash冲突使用的是 拉链发, 注意每次新元素插入链表时,使用的是头插法,O(1)复杂度。
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{
		/* ...... */
    ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
  
    entry = zmalloc(sizeof(*entry));
    entry->next = ht->table[index];
    ht->table[index] = entry;
    ht->used++;
    /* ...... */
    return entry;
}

如果发生大量的 hash冲突,就会导致 链表的长度越长,访问复杂度最坏O(hash链表长度),严重的影响速度,所以rehash就登场了,

  • rehash 简单说就是,hash表扩容,将ht[0]的元素迁移到ht[1]中,减少hash冲突,增加访问效率。

但并不是一次迁移完毕,而是,分多次,渐进式的,断续进行,这样才不会对reids的主线程造成影响。

渐进式rehash是批量拷贝,每次只拷贝hash表中n(1)个buckt,这就对主线程的影响也就有限了。

渐进式rehash过程

  • 指定允许访问最多的空bucket的数量 n*10

  • 循环访问N个buckt,并且还有可用节点,复制 bucket中的节点到 ht[1]中

    • ht[0]的size 必须大于 rehahsidx

    • 循环得到一个不为空的bucket

      • 如果 为空的bucket 大于等于N*10,则直接return结束,避免时间过长影响服务性能。
    • 将该bucket中的的节点迁移到ht[1]中bueckt中,

    • ht[0].used--,ht[1]used++

  • 检查ht[0]中是否没有可用节点used(表示全部复制完毕了)

    • 释放ht[0]空间
    • 将ht[1]复制给ht[0]
    • 释放ht[1]空间
    • 设置rehashidx=-1,表示本次rehash结束。

dictRehash流程.png


// empty_visits 最大允许访问空桶的数量,超过可能会影响性能。
// n:要拷贝 bucket的数量。
int dictRehash(dict *d, int n) {
    int empty_visits = n*10; /* Max number of empty buckets to visit. */ // 允许的最大的空 buckets数量。
    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);
      
      	// 得到一个不为空的 bucket,
      	// 如果 为空数量 超过了 empty_visits ,则提前退出,避免造成性能影响。
        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 */
      	// 将 该 bucket中 单链表 copy到 新的 ht[1]中去。
        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;
        }
      	// 完成后将 ht[0]table[rehashidx]置为nil
	      // rehashidx++ ,接着下一个
        d->ht[0].table[d->rehashidx] = NULL;
        d->rehashidx++;
    }

    /* Check if we already rehashed the whole table... */
  	// 最后在检查一下,ht[0]是否没有可用的key
    if (d->ht[0].used == 0) {
        zfree(d->ht[0].table); // 释放 ht[0]空间
        d->ht[0] = d->ht[1]; // 将 ht[1] 赋值给 ht[0]
        _dictReset(&d->ht[1]); // 重置 ht的大小为0
        d->rehashidx = -1; // 设置全局hash表,rehashidx=-1,表示rehash结束
        return 0; // 返回0 表示 ht[0]中的所有元素都迁移完毕
    }

    /* More to rehash... */
    return 1; // 表示 ht[0]的元素,还没后全部迁移完毕。
}

渐进式rehash优点

分而治之的方式,将rehash的工作分派到,添加,删除,查找和更新的操作上,从而避免集中式rehash,而带来的的庞大计算。

字典的删除(delete)、查找(find)、更新(update)等操作会在两个哈希表上进行,

字典的添加(add)则只会添加到 ht[1]中去

dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing){
  	/*....*/
    ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
    /*....*/
    return entry;
}

// 删除,从ht[0] 和 ht[1]中去删除
static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) {
		 /*....*/
    for (table = 0; table <= 1; table++) {
        idx = h & d->ht[table].sizemask;
        he = d->ht[table].table[idx];
    }
  	/*....*/
}

dictEntry *dictFind(dict *d, const void *key){
		 /*....*/
    for (table = 0; table <= 1; table++) {
        idx = h & d->ht[table].sizemask;
        he = d->ht[table].table[idx];
    }
    /*....*/
}

函数调用关系

_dictRehashStep调用 dictRehash 传入的循环次数都是1,

static void _dictRehashStep(dict *d) {
    if (d->iterators == 0) dictRehash(d,1);
}

image.png

Q&A

rehash的触发和 渐进式rehash的有啥关系呢?

我们结合一个 【dictAddRaw】函数来分析一下

  • dictIsRehashing 首先它会判断改字典 是否正处于 rehashing的状态中,
    • 如果是 则进行 ht[0]数据 rehashing 到 ht[1]中 (渐进式rehash操作)
  • _dictKeyIndex 找到该 新key的位置,如果需要扩容则会扩容字典,并设置 全局的 rehashidx=0

也就是说,只有 字典发生了 扩容设置了 rehasidx=0,则 渐进式rehash才会触发

dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{
    long index;
    dictEntry *entry;
    dictht *ht;

    if (dictIsRehashing(d)) _dictRehashStep(d);

    /* Get the index of the new element, or -1 if
     * the element already exists. */
    if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)
        return NULL;

    /* Allocate the memory and store the new entry.
     * Insert the element in top, with the assumption that in a database
     * system it is more likely that recently added entries are accessed
     * more frequently. */
    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;
}