分布式key/value cache redis(version: 2.4.2) 源码学习(二)--------dict(哈希表)学习

      在程序中很多地方我们会用到哈希表,因为它可以实现快速的增删查改操作。redis中也多次用到了哈希表,这几天仔细看了一下其中哈希表的实现dict.h和dict.cpp文件,这里对它的源码进行一些解析。
        哈希表最重要的无非就这几样东西,一个是哈希函数的性能和计算散列值的分布情况,另外一个是如何处理冲突,最后一个是当负载率越来越高导致性能下降的时候,如何重构哈希表。现在对哈希函数的研究已经做的很深入了,有一些非常好的哈希函数我们拿来就可以用,性能和散列值的分布也都还不错。处理冲突一般都使用简单的链式方法。这个里面有一个矛盾的地方,哈希表的负载率比较低的话,增删查改的效率比较高,但是会有大量的内存浪费;如果负载率比较高,内存使用的效率比较高的话,增删查改的效率就会受到影响,这就是计算机里面典型的时间和空间互相转换的列子。
        当哈希表的负载率越来越高的时候,就需要对哈希表进行一次重构,其实就是扩大哈希表中bucket的个数,使得同一个bucket中的元素数量减少。然后对原来哈希表中的元素进行一次重新哈希放入新的哈希表中。但是这个操作往往是非常耗时的,如果要一次完成的话,用户可能得等待很长的时间。例如一个哈希表当用户调用add函数增加一个元素的时候,发现哈希表的负载率太高了,要进行扩容,然后进行扩容后,在执行add操作,那么这个哈希表的插入性能就是不稳定的,也是不可接受的。
        redis的哈希表将重构的代价(这个里面叫rehash)分摊到一个一个的add, delete, find操作中去了,它在内部维护了两个哈希表,dictht[0], dictht[1], 当它发现dictht[0]的负载率比较高的时候,它就开始不断地在每一次add, delete, find这些操作中把dictht[0]中的元素迁移到dictht[1]中去。此时dictht[1]的bucket数目是dictht[0]的两倍。当dictht[0]中的元素全部迁移到dictht[1]中的时候,就将dictht[1]赋值给dictht[0], dictht[1]赋为空。然后随着元素数目的增多,dictht[0]的负载率又达到规定的上限的时候,dictht[1]又申请一个bucket数目为dictht[0]两倍容量,继续搬迁的过程,一直不断迭代下去。需要注意的是redis将元素从dictht[0]向dictht[1]中搬迁的过程不是一次完成的,所以有时候dictht[0], dictht[1]中都有元素。这时候增加的元素(dictht[0], dictht[1]中都没有)会增加到dictht[1]中去,删除操作还是元素在dictht[0], dictht[1]中那个就在那个里面操作。查找两个都要查,修改先要找到,在那个表就会在那个里面进行修改。
      主要的思想就是上面那些,下面开始分析一下源码:

//下面的结构表示一个哈希表,一个dict中有两个哈希表
typedef struct dictht {
    dictEntry **table;           //bucket数组,一个bucket是一个dictEntry的列表
    unsigned long size;      //哈希表中bucket的个数
    unsigned long sizemask;   //实际的值等于size - 1, 目的类似于对hash函数求出来的值求余,避免了求模的操作,求模操作的代价比                                                      //较高,改用位运算操作进行
    unsigned long used;    //哈希表中现在的元素个数
} dictht;

typedef struct dict {
    dictType *type;     //指定哈希函数,key的比较函数, free函数等
    void *privdata;      //我看的2.4.2版本中好像没有什么用
    dictht ht[2];            //两个哈希表
    int rehashidx;       //记录将dictht[0]中的bucket向dictht[1]中搬迁到那个index了,等于-1的话,说明现在没有进行搬迁
    int iterators;          // dict现在有几个迭代器在进行工作
} dict;

//迭代器, 如果safe == 1, 迭代的过程中还可以对dict进行增删查等操作,否则只能调用next操作

typedef struct dictIterator {
    dict *d;
    int table, index, safe;            
    dictEntry *entry, *nextEntry;   //当前元素,以及下一个元素
} dictIterator;
整个结构如下(图转载自http://www.cnblogs.com/kernel_hcy/archive/2011/05/18/2050421.html):


        除了这些数据结构之外,redis定义了很多相关的宏,dictIsRehashing, dictSlots, dictSize等。

        下来看看主要的函数:
先看private函数:

static unsigned long _dictNextPower(unsigned long size)   //用于计算扩容后哈希表的大小
static void _dictPrintStatsHt  //打印一些dict的统计信息,一共有多少个元素,多少个slots(也就是多少个bucket),平均每个bucket有几个元素等

//这个函数返回值的含义自己不太明白,那位大牛解释一下? 自己的理解是要不要expand, 但是在_dictKeyIndex函数中_dictExpandIfNeeded(d) == DICT_ERR 就返回了-1, 说明元素已经在dict中,不太理解这块
static int _dictExpandIfNeeded(dict *d) {
    if (dictIsRehashing(d)) return DICT_OK;
    //如果dict为空,那么将其大小设置为DICT_HT_INITIAL_SIZE
    if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);
    //如果dictht[0]的负载率达到1并且(可以resize或者负载率达到必须进行resize的情况)
    if (d->ht[0].used >= d->ht[0].size &&
        (dict_can_resize ||
         d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
    {

        //不太能看懂这一句,能进入这个if, 说明d->ht[0].used >= d->ht[0].size
        //那么((d->ht[0].size > d->ht[0].used) ? d->ht[0].size : d->ht[0].used)*2) 的值一定等于
d->ht[0].used)*2, 为什么要搞这么复杂,有劳
// 大牛解释

        return dictExpand(d, ((d->ht[0].size > d->ht[0].used) ?
                                    d->ht[0].size : d->ht[0].used)*2);
    }
    return DICT_OK;
}

返回key在dict应该插入的bucket的index
static int _dictKeyIndex(dict *d, const void *key)
{
    unsigned int h, idx, table;
    dictEntry *he;

   //这一句看不太懂,_dictExpandNeeded() == DICT_ERR就回-1, 而-1表示d中已经包含该元素, 这个逻辑不太明白,那位大牛给解释一下?
    if (_dictExpandIfNeeded(d) == DICT_ERR)
        return -1;
    
    h = dictHashKey(d, key);
    for (table = 0; table <= 1; table++) {
        idx = h & d->ht[table].sizemask;
        he = d->ht[table].table[idx];
        while(he) {
            if (dictCompareHashKeys(d, key, he->key))
                return -1;
            he = he->next;
        }
        if (!dictIsRehashing(d)) break;
    }
    return idx;
}

static void _dictRehashStep(dict *d)                 迁移d->dictht[0]中的一个bucket到d->dictht[1]中去
int dictRehash(dict *d, int n)                    迁移d->dictht[0]中n个bucket到d->dictht[1]中去,1表示dictht[0]还有需要迁移的元素, 否则没有

最后详细看一个dictAdd函数,其他的dictFind, dictDelete都和这个差不多

int dictAdd(dict *d, void *key, void *val)
{
    int index;
    dictEntry *entry;
    dictht *ht;
    if (dictIsRehashing(d)) _dictRehashStep(d);   //判断是否需要搬迁元素,redis将搬迁元素的代价分散到一次次的add, delete, find中去了
    //对_dictKeyIndex函数的返回值还是有疑问,源码上的注释是返回-1表示元素已经存在
    if ((index = _dictKeyIndex(d, key)) == -1)
        return DICT_ERR;
    //如果正在rehashing,那么新元素插入到ht[1]中去
    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++;
    //设置entry的key和value
    dictSetHashKey(d, entry, key);
    dictSetHashVal(d, entry, val);
    return DICT_OK;
}

         最后需要说明的是,dict相关的函数都没有进行多线程控制,用户如果要使用多线程操作,要自己进行控制。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值