Redis数据结构和源码分析——Dict

104 阅读7分钟

结构

typedef struct dict {
    dictType *type; //dict类型,内置不同hash函数
    void *privdata; //私有数据,在做特殊hash运算时用
    dictht ht[2]; //一个dict包含两个哈希表,另一个hash表在rehash时用
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */ //rehash进度索引,
    每进行一次rehash都会更新
    //rehash是否暂停
    int16_t pauserehash; /* If >0 rehashing is paused (<0 indicates coding error) */
} dict;
typedef struct dictType {
    // 哈希函数,用于计算键的哈希值
    uint64_t (*hashFunction)(const void *key);
    // 复制键的函数,用于复制键的值
    void *(*keyDup)(void *privdata, const void *key);
    // 复制值的函数,用于复制值的内容
    void *(*valDup)(void *privdata, const void *obj);
    // 比较键的函数,用于比较两个键的值是否相等
    int (*keyCompare)(void *privdata, const void *key1, const void *key2);
    // 键的析构函数,用于释放键的内存
    void (*keyDestructor)(void *privdata, void *key);
    // 值的析构函数,用于释放值的内存
    void (*valDestructor)(void *privdata, void *obj);
    // 扩容允许性检查函数,用于判断是否允许进行扩容
    int (*expandAllowed)(size_t moreMem, double usedRatio);
} dictType;

typedef struct dictEntry {
    void *key; //键
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v; //值
    //下一个Entry指针
    struct dictEntry *next;
} dictEntry;
typedef struct dictht {
    //entry数组,数组中保存的是指向entry的指针
    dictEntry **table;
    //哈希表大小
    unsigned long size;
    //哈希表掩码,总等于size - 1  所以h & sizemask即可运算索引位置
    unsigned long sizemask;
    //used为哈希表中键值对总和,每做一次add操作used就会加1
    unsigned long used;
} dictht;

image.png 哈希碰撞时结构图,头插法,新entry插到链表头,next指针指向旧的entry

image.png 总结构图

扩容

int _dictExpand(dict *d, unsigned long size, int* malloc_failed) {
    // 如果 malloc_failed 不为 NULL,则初始化为 0
    if (malloc_failed) *malloc_failed = 0;

    /* 如果正在进行 rehash 或者新的大小小于已使用元素的数量,返回错误 */
    if (dictIsRehashing(d) || d->ht[0].used > size)
        return DICT_ERR;

    dictht n; /* 新的哈希表 */
    unsigned long realsize = _dictNextPower(size); //这里会获取新的大小

    /* 检测溢出 */
    if (realsize < size || realsize * sizeof(dictEntry*) < realsize)
        return DICT_ERR;

    /* 如果新的大小等于当前哈希表的大小,返回错误。收缩时可能发生 */
    if (realsize == d->ht[0].size) return DICT_ERR;

    /* 分配新的哈希表并将所有指针初始化为 NULL */
    n.size = realsize;
    n.sizemask = realsize - 1;
    
    if (malloc_failed) {
        // 如果 malloc_failed 不为 NULL,使用 ztrycalloc 分配内存
        n.table = ztrycalloc(realsize * sizeof(dictEntry*));
        *malloc_failed = n.table == NULL;
        if (*malloc_failed)
            return DICT_ERR;
    } else {
        // 否则,使用 zcalloc 分配内存
        n.table = zcalloc(realsize * sizeof(dictEntry*));
    }

    n.used = 0;

    /* 如果是第一次初始化,只是设置第一个哈希表以便接受键,不算真正的 rehashing */
    if (d->ht[0].table == NULL) {
        d->ht[0] = n;
        return DICT_OK;
    }

    /* 准备第二个哈希表用于增量式 rehashing ,准备rehash*/
    d->ht[1] = n;
    d->rehashidx = 0;
    return DICT_OK;
}

  1. _dictNextPower 函数用于找到大于等于 size 的最小的 2 的幂。这是为了确保哈希表的大小始终是 2 的幂,这有助于提高哈希函数的性能,因为取模运算可以通过位运算进行优化。
  2. 如果 size 已经是 2 的幂,那么 realsize 的值就等于 size,不进行额外的变化。
  3. 如果 size 不是 2 的幂,那么 realsize 的值就会是大于等于 size 的最小的 2 的幂。
/* 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; //永远是2的n次
    }
}

加入一个键值对时发生了什么

int dictAdd(dict *d, void *key, void *val)
{
    dictEntry *entry = dictAddRaw(d,key,NULL); //返回的entry有key无value
    if (!entry) return DICT_ERR;
    dictSetVal(d, entry, val);  //entry返回后再设置值
    return DICT_OK;
}
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing) {
    long index;
    dictEntry *entry;
    dictht *ht;

    // 如果正在进行 rehash 操作,则执行一步 rehash
    if (dictIsRehashing(d)) _dictRehashStep(d);

    // 获取键的索引,如果键已经存在,则返回 -1,存在的情况下可以通过 existing 获取已存在的节点
    if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)
        return NULL;

    // 获取当前哈希表,如果正在rehash就获取新表
    ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];

    // 分配内存并存储新的 entry
    entry = zmalloc(sizeof(*entry));
    entry->next = ht->table[index];
    ht->table[index] = entry;
    ht->used++;  //每加入一个元素used就会加1

    // 设置 hash entry 的字段
    dictSetKey(d, entry, key);

    // 返回新添加的 entry
    return entry;
}

int dictRehash(dict *d, int n) {
    int empty_visits = n * 10; /* 最大访问空桶次数。这里是10*/
    if (!dictIsRehashing(d)) return 0; //如果rehash结束了

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

        /* 确保 rehashidx 不会溢出,因为还有更多元素要重新哈希。*/
        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];

        /* 将该桶中的所有键从旧哈希表移动到新哈希表。*/
        while (de) {
            uint64_t h;

            nextde = de->next;

            /* 在新哈希表桶的头部插入键。*/
            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++;
    }

    /* 检查是否已经完成整个哈希表的 rehash... */
    if (d->ht[0].used == 0) {
        /* 释放旧哈希表,用新哈希表替换,并重置 rehash 状态。 */
        zfree(d->ht[0].table);
        d->ht[0] = d->ht[1];
        _dictReset(&d->ht[1]);
        d->rehashidx = -1;
        return 0; /* Rehash 操作已完成。*/
    }

    /* 还有更多要 rehash... */
    return 1; /* 仍有更多工作要做。 */
}

添加一个元素时会检查是否在rehash,如果在rehash会进行一次rehash,如果连续十次访问到的都是空桶会停止rehash。在rehash中会重新对一个桶中的所有元素进行重新散列,直到清空旧表中此旧桶的元素。每次操作新旧桶都会做一次used++或--的操作,rehash结束后会检查是否全部rehash完毕,如果已经全部完毕会更改rehash状态,并释放旧表内存。

如果没有在rehash,首先检查键是否已经存在,然后创建一个只有键的entry,包装返回后插入value。

如果有一个重复的Key插入,并且插入时发现正在rehash,于是键值对插入在新表中。当旧表中重复的key要迁移到新表,迁移时并没有做检查逻辑,会不会造成数据重复呢?

一番阅读后找到了这块代码,在判断Key是否存在时,如果正在rehash,新表旧表都会判断,所以不会出现数据重复的情况。

static long _dictKeyIndex(dict *d, const void *key, uint64_t hash, dictEntry **existing)
{
    unsigned long idx, table;
    dictEntry *he;
    if (existing) *existing = NULL;

    /* Expand the hash table if needed */
    if (_dictExpandIfNeeded(d) == DICT_ERR)
        return -1;
    for (table = 0; table <= 1; table++) {
        idx = hash & d->ht[table].sizemask;
        /* Search if this slot does not already contain the given key */
        he = d->ht[table].table[idx];
        while(he) {
            if (key==he->key || dictCompareKeys(d, key, he->key)) {
                if (existing) *existing = he;
                return -1;
            }
            he = he->next;
        }
        if (!dictIsRehashing(d)) break;
    }
    return idx;
}

删除一个键值对时发生了什么

/* Remove an element, returning DICT_OK on success or DICT_ERR if the
 * element was not found. */
int dictDelete(dict *ht, const void *key) {
    return dictGenericDelete(ht,key,0) ? DICT_OK : DICT_ERR;
}
/* 从字典中删除键为 key 的元素,如果 nofree 为 0,则同时释放键和值的内存。*/
static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) {
    uint64_t h, idx;
    dictEntry *he, *prevHe;
    int table;

    /* 如果哈希表为空,直接返回。*/
    if (d->ht[0].used == 0 && d->ht[1].used == 0) return NULL;

    /* 如果正在进行 rehash 操作,则执行一步 rehash。*/
    if (dictIsRehashing(d)) _dictRehashStep(d);

    /* 计算键的哈希值。*/
    h = dictHashKey(d, key);

    /* 遍历两个哈希表,查找并删除元素。*/
    for (table = 0; table <= 1; table++) {
        idx = h & d->ht[table].sizemask; /* 计算索引位置。*/
        he = d->ht[table].table[idx]; /* 获取链表头。*/
        prevHe = NULL;

        while (he) {
            /* 如果找到匹配的键,则删除元素。*/
            if (key == he->key || dictCompareKeys(d, key, he->key)) {
                /* 从链表中解除该元素的链接。*/
                if (prevHe)
                    prevHe->next = he->next;
                else
                    d->ht[table].table[idx] = he->next;

                /* 如果需要释放内存,则同时释放键和值的内存。*/
                if (!nofree) {
                    dictFreeKey(d, he);
                    dictFreeVal(d, he);
                    zfree(he);
                }
                
                //更新used
                d->ht[table].used--;

                /* 返回被删除的元素。*/
                return he;
            }

            prevHe = he;
            he = he->next;
        }

        /* 如果不是正在进行 rehash 操作,则只遍历第一个哈希表。*/
        if (!dictIsRehashing(d))
            break;
    }

    return NULL; /* 未找到匹配的元素。*/
}

所以增加或移除一个元素都会检查是否在rehash,而rehash也许在扩容也许在收缩,如果在收缩会释放掉该释放的内存