Redis源码分析——字典(dict)

  • Redis版本:5.0.5
  • 文件:dict.h dict.c

字典概念

  • 字典,又称为符号表,关联数组或映射,是一种用于保存键值对的抽象数据结构。
  • 在字典中,一个键可以和一个值进行关联(或者说为映射),这些关联的键和值就称为键值对。
  • 字典中的每个键都是独一无二的,程序可以在字典中根据键查找与之关联的值,或者通过键来更新值,又或者根据键来删除整个键值对等。

字典的结构

  • 哈希表
typedef struct dictht {
    //哈希表数组
    dictEntry **table;
    //哈希表的大小
    unsigned long size;
    //哈希表的掩码,用于计算索引值,并且其大小总是等于size-1
    unsigned long sizemask;
    //哈希表已有节点的数量
    unsigned long used;
} dictht;
  • 哈希表节点
typedef struct dictEntry {
    //键
    void *key;
    //值(值可以是指针,可以是unit64_t, int64_t, double)
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    //指向下一个哈希表节点,形成链表
    struct dictEntry *next;
} dictEntry;
  • dictype
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);
} dictType;
  • 字典dict
typedef struct dict {
    //函数结构体
    dictType *type;
    //私有数据
    void *privdata;
    dictht ht[2]; //两个数组,ht[0]用来hash,ht[1]用来rehash
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    unsigned long iterators; /* number of iterators currently running 迭代器*/
} dict;

字典的创建、插入、Rehash、删除查找等函数(很详细的在代码中进行注释讲解)

1.创建函数

//dictCreate暴露给用户 
//该函数只是给dict分配了空间,而其内部的ht->table还没有分配空间,等到第一次添加键值对时分配
dict *dictCreate(dictType *type,
        void *privDataPtr)
{
    dict *d = zmalloc(sizeof(*d));

    _dictInit(d,type,privDataPtr);
    return d;
}

/* Initialize the hash table */
int _dictInit(dict *d, dictType *type,
        void *privDataPtr)
{
	//_dicRest函数初始化dictht
    _dictReset(&d->ht[0]);
    _dictReset(&d->ht[1]);
    d->type = type;
    d->privdata = privDataPtr;
    d->rehashidx = -1;
    d->iterators = 0;
    return DICT_OK;
}

2.插入函数

  • dictAdd函数是暴露给用户的函数,其内部调用了dictAddRaw函数来进行添加
int dictAdd(dict *d, void *key, void *val)
{
    dictEntry *entry = dictAddRaw(d,key,NULL);

    if (!entry) return DICT_ERR;
    dictSetVal(d, entry, val);
    return DICT_OK;
}
  • dicAddRaw函数
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{
    long index;
    dictEntry *entry;
    dictht *ht;
    
	//判断是否该dict是否正在rehash,如果是则调用_dictRehashStep函数
    if (dictIsRehashing(d)) _dictRehashStep(d);

	//调用_dictKeyIndex获取要插入的位置,下面会讲到这个函数
    if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)
        return NULL;

	//如果正在rehash则插入到ht[1]中,否则插入的ht[0]中。
    ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
    entry = zmalloc(sizeof(*entry));
    entry->next = ht->table[index];
    ht->table[index] = entry;
    ht->used++;

    dictSetKey(d, entry, key);
    return entry;
}
  • _dictKeyIndex函数
static long _dictKeyIndex(dict *d, const void *key, uint64_t hash, dictEntry **existing)
{
    unsigned long idx, table;
    dictEntry *he;
    if (existing) *existing = NULL;

    //判断是否还需要额外的空间,下面会将这个函数
    if (_dictExpandIfNeeded(d) == DICT_ERR)
        return -1;
    for (table = 0; table <= 1; table++) {
        //算出插入的位置idx
        idx = hash & d->ht[table].sizemask;
        //找到idx位置的头结点
        he = d->ht[table].table[idx];
        //判断idx位置的链表中是否有相同的key
        while(he) {
            if (key==he->key || dictCompareKeys(d, key, he->key)) {
                if (existing) *existing = he;
                return -1;
            }
            he = he->next;
        }
        /*
        *没有正在rehash的话就只需要判断ht[0]就可以
        *如果正在rehash,则还需要判断ht[1]中是否也有相同的键
        */
        if (!dictIsRehashing(d)) break;
    }
    //返回要插入的位置
    return idx;
}
  • _dictExpandIfNeeded函数
static int _dictExpandIfNeeded(dict *d)
{
	//如果正在rehash,则直接返回DICT_OK(因为正在rehash表明有足够的空间)
    if (dictIsRehashing(d)) return DICT_OK;
    
    //如果是第一次插入,则需要给ht[0]分配空间,调用dicExpand函数(这就是上面将创建dict时没有给ht->table分配空间在这里进行创建)
    if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE); //这个函数下面也会讲

    //当已有的元素大于size或者大于初始装载因子时则需要扩容
    if (d->ht[0].used >= d->ht[0].size &&
        (dict_can_resize ||
         d->ht[0].used/d->ht[0].size > dict_force_resize_ratio)) 
    {
        //used*2 扩容两倍,调用dictExpand函数
        return dictExpand(d, d->ht[0].used*2);
    }
    return DICT_OK;
}

  • dictExpand函数
int dictExpand(dict *d, unsigned long size)
{
    //扩容, d正在rehash或者已有元素大于扩容的容量 
    if (dictIsRehashing(d) || d->ht[0].used > size)
        return DICT_ERR;

    //new hash table 
    dictht n; 
    //返回新hashtable的容量 _dictNextPower函数比较简单,可以自行去阅读(该函数总是返回距离size最近的大于它的2的n次方的数)
    //举个例子:扩容的size = 6,该函数就会返回8。  size = 17,该函数就会返回32。 
    unsigned long realsize = _dictNextPower(size);

    
    if (realsize == d->ht[0].size) return DICT_ERR;

    //给新的hash table分配空间
    n.size = realsize;
    n.sizemask = realsize-1;
    n.table = zcalloc(realsize*sizeof(dictEntry*));
    n.used = 0;

	//如果ht[0]还没有分配空间,则n赋值给ht[0]。
    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 表明要开始rehash操作
    d->rehashidx = 0;
    return DICT_OK;
}
  • 总结一下插入的流程。
    1.第一次插入: 调用插入函数,发现是第一次插入,就需要给ht[0]分配内存,然后找到要插入的下标进行插入。
    2.之后的插入:调用插入函数,判断空间是否还能插入,可以插入则直接插入。空间不足,就需要新的table,就为ht[1]分配空间,然后将rehashidx置为0,进行rehash,这个时候的插入都将会插入到ht[1]中。

3.Rehash函数

  • _dictRehashStep函数(这个函数在上文中的dictAddRaw函数中出现)
static void _dictRehashStep(dict *d) 
{
    if (d->iterators == 0) dictRehash(d,1);
}
  • dictRehash函数
int dictRehash(dict *d, int n) {
    //一次rehash n*10个桶的元素
    int empty_visits = n*10; /* Max number of empty buckets to visit. */
    if (!dictIsRehashing(d)) return 0;

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

        assert(d->ht[0].size > (unsigned long)d->rehashidx);
        //当前桶已经为空,将rehashidx++
        while(d->ht[0].table[d->rehashidx] == NULL) {
            d->rehashidx++;
            //本次rehash已经结束,但是hash[0]中还有元素,return 1,表明还需要继续rehash操作。
            if (--empty_visits == 0) return 1;
        }
        de = d->ht[0].table[d->rehashidx];
        //将ht[0].table[d->rehashidx]桶的元素移动到ht[1]中
        while(de) {
            uint64_t h;

            nextde = de->next;
            //需要重新计算在ht[1]中的位置
            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) {
        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;
}
  • 总结rehash
    从上面的代码中可以看出,Redis中的字典是采用渐进式rehash方法,如果ht[0]中元素非常多,采用渐进式rehash不会导致rehash操作浪费太多时间而导致服务器阻塞一段时间。

掌握了上文中讲的所有函数后,大家自行去看删除查找的源码就没有什么难度了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值