四、Redis源码数据结构之哈希表Hash

Redis作为一款内存数据库,解决内存性能问题就显得尤为重要。作为Hash表这种应用数据结构,当数据量大的时候,就会出现2大问题:哈希冲突rehash开销

一、什么是哈希冲突以及redis如何解决哈希冲突

哈希表是基于数组的一种存储方式.它主要由哈希函数和数组构成。

当要存储一个数据的时候,首先用一个函数计算数据的地址,然后再将数据存进指定地址位置的数组里面。这个函数就是哈希函数,而这个数组就是哈希表。

哈希表的优势在于:相比于简单的数组以及链表,它能够根据元素本身在第一时间,也就是时间复杂度为0(1)内找到该元素的位置。这使得它在查询和删除、插入上会比数组和链表要快很多。因为他们的时间复杂度为o(n)。

哈希冲突是指哈希函数算出来的地址被别的元素占用了,也就是,这个位置有人了。而解决哈希冲突的办法之一就是开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法

哈希函数用的就是链地址法,也就是数组+链表的方法,hashMap用的就是链地址法.。链地址法就是,当没有发生哈希冲突的时候hashmap主要只有数组。但是当发生冲突的时候,它会在哈希函数找到的当前数组内存地址位置下添加一条链表。

Redis所采用的正是这样链地址法-所谓的链式哈希

相关源码文件:dict.c 和 dict.h

/* 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; 二维数组
    unsigned long size; 表大小
    unsigned long sizemask; hash取模的用的
    unsigned long used; 元素个数
} dictht;


typedef struct dictEntry { // 哈希实体
    void *key; key键
    union { value值
        void *val;
        uint64_t u64; 无符合
        int64_t s64; 有符号
        double d;
    } v;  联合体V
    struct dictEntry *next; 下一个哈希实体
} dictEntry;

补充一句:伴随着链表的增长性能也就随之变慢了

 

二、rehash详细介绍以及Redis如何解决rehash造成的性能问题

rehash是在hash表的大小不能满足需求,造成过多hash碰撞后需要进行的扩容hash表的操作,

其实通常的做法确实是建立一个额外的hash表,将原来的hash表中的数据在新的数据中进行重新输入,从而生成新的hash表。

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2]; 2个hash表,交替使用
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */ -1标识无需rehash
    unsigned long iterators; /* number of iterators currently running */
} dict;

1、触发rehash:

 

 /* Expand the hash table if needed */
static int _dictExpandIfNeeded(dict *d)
{
/* Incremental rehashing already in progress. Return. */ 正在进行rehashing 则直接返回 if (dictIsRehashing(d)) return DICT_OK; /* If the hash table is empty expand it to the initial size. */ 条件一:如果哈希表为空,将其扩展为初始大小4。 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. */
// 如果我们达到1:1的比例,我们可以调整哈希标的的大小(全局设置)或我们应该避免它但之间的比率元素/桶超过“安全”阈值时,我们将大小加倍桶的数量。
if (d->ht[0].used >= d->ht[0].size && (dict_can_resize || d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
条件二: 承载的元素个数大于等于表大小,且允许扩容 dict_can_resize 1
条件三: 承载元素与表大小比率大于安全阈值 dict_force_resize_ratio 5 (负载因子factor) {
return dictExpand(d, d->ht[0].used*2); } return DICT_OK; }
void dictEnableResize(void) {
    dict_can_resize = 1; 启用
}

void dictDisableResize(void) {
    dict_can_resize = 0; 禁止
}
/* This function is called once a background process of some kind terminates,
 * as we want to avoid resizing the hash tables when there is a child in order
 * to play well with copy-on-write (otherwise when a resize happens lots of
 * memory pages are copied). The goal of this function is to update the ability
 * for dict.c to resize the hash tables accordingly to the fact we have o not
 * running childs. */
void updateDictResizePolicy(void) {
    if (server.rdb_child_pid == -1 && server.aof_child_pid == -1) 该调用方法在server.c文件中,条件:没有rdb快照、aof重写场景才启用rehash
        dictEnableResize();
    else
        dictDisableResize();
}

int hasActiveChildProcess() {
    return server.rdb_child_pid != -1 ||
           server.aof_child_pid != -1;
}

 

2、rehash扩容大小:

dictExpand函数入参:扩容的表,扩容容量

dictExpand(d, DICT_HT_INITIAL_SIZE); DICT_HT_INITIAL_SIZE 4
dictExpand(d, d->ht[0].used*2);
/* 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 */
    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;
    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;
    return DICT_OK;
}
/* Our hash table capability is a power of two */
static unsigned long _dictNextPower(unsigned long size)
{
    unsigned long i = DICT_HT_INITIAL_SIZE;

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

 

3、rehash执行过程:

 

int dictRehash(dict *d, int n) {
    int empty_visits = n*10; /* Max number of empty buckets to visit. */
    .....while(n-- && d->ht[0].used != 0) { 根据变量n,循环拷贝迁移数据
        .... 具体见rehashidx变量讲解
    }

    /* Check if we already rehashed the whole table... */
    if (d->ht[0].used == 0) {  判断ht[0]数据是否迁移完成
        zfree(d->ht[0].table); 迁移完成释放ht[0]内存空间
        d->ht[0] = d->ht[1];   赋值
        _dictReset(&d->ht[1]);  重置ht[1]大小为0
        d->rehashidx = -1;  全局rehash标识 -1标识rehash结束
        return 0;  返回0 所有元素迁移完
    }

    /* More to rehash... */
    return 1;  ht[0]元素未迁移完
}

rehashidx变量讲解:变量表示当前rehash在对哪个桶bucket做数据迁移,从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; 当检查的空桶(桶中没有元素)数量超过设定的值,停止rehash,转而继续做其他事情,渐进式rehash的由来,从而优化Redis性能
        }
        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++;

 

static void _dictRehashStep(dict *d) {
    if (d->iterators == 0) dictRehash(d,1); // 循环次数为1 从而执行完一次rehash,redis既可以处理其他请求,不会一直占用线程导致主线程阻塞 ---渐进式rehash
}

对于C语言不是很感冒 的我也只能这么简单的看下源码,没有太细化,对于hash有兴趣的可以自行查找资料学习下,感谢。

 

posted @ 2022-05-11 22:35  chch213  阅读(150)  评论(0编辑  收藏  举报