天天看点

dictdict.h dict.c

dict.h dict.c

dict是redis中定义的一个hashtable。

它的数据结构是这样的

typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;

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;

typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[];
    long rehashidx; 
    unsigned long iterators; 
} dict;
           

dictEntry是hashtable中的节点,它保存了value和key,还有一个指向另一个dictEntry的指针,指向另一个和他被分到同一个数组节点的dictEntry。

dictType则保存了一些hashtable需要用到的函数的指针。

dictht则是整个hashtable中实际保存数据的表。这样的表在同一个hashtable中有两个。多出来的一个他们会在扩容的时候被用到,我们在后面会介绍。table字段则是一个指向dictEntry数组的指针,size是数组的大小,sizemask是给dictEntry安排位置的掩码,用来替代求余运算,used是当前装载的dictEntry数量。

正主dict包含了一个dictType,reshashidx表示扩容的进度,iterators表示dict的迭代器。

dict的初始化

static void _dictReset(dictht *ht)
{
    ht->table = NULL;
    ht->size = ;
    ht->sizemask = ;
    ht->used = ;
}
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)
{
    _dictReset(&d->ht[]);
    _dictReset(&d->ht[]);
    d->type = type;
    d->privdata = privDataPtr;
    d->rehashidx = -;
    d->iterators = ;
    return DICT_OK;
}
           

没什么特别的地方,初始化各字段。

int dictResize(dict *d)
{
    int minimal;
    //检查是否可以扩容
    if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;
    //检查是否是dictht的初始化,如果是则按照dictht的最小空间扩容。
    minimal = d->ht[].used;
    if (minimal < DICT_HT_INITIAL_SIZE)
        minimal = DICT_HT_INITIAL_SIZE;
    return dictExpand(d, minimal);
}

int dictExpand(dict *d, unsigned long size)
{
    dictht n; 
    //获取扩容到目标,每次扩容到原来的2倍,_dictNextPower就是下一个2次幂
    unsigned long realsize = _dictNextPower(size);

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

    n.size = realsize;
    //reasize是2次幂,股realsize-1用二进制表示则每一位上都是1。
    n.sizemask = realsize-;
    //给dictht中的dictEntry数组申请空间
    n.table = zcalloc(realsize*sizeof(dictEntry*));
    n.used = ;

    //如果是dictht的初始化,新生成的dictht则交给ht[0]指针
    if (d->ht[].table == NULL) {
        d->ht[] = n;
        return DICT_OK;
    }
    //如果不是,则把新生成的dictht交给ht[1]指针
    d->ht[] = n;
    d->rehashidx = ;
    return DICT_OK;
}
           

dictResize是个很有意思的过程。它根据新的大小生成新的dictht然后直接把新的dict交给ht[1]指针,同时维护了两个表。然后通过dictRehash迁移数据

int dictRehash(dict *d, int n) {
    //设置迁移数量
    int empty_visits = n*;
    if (!dictIsRehashing(d)) return ;
    //迁移循环
    while(n-- && d->ht[0].used != 0) {
        dictEntry *de, *nextde;

        assert(d->ht[].size > (unsigned long)d->rehashidx);
        while(d->ht[].table[d->rehashidx] == NULL) {
            d->rehashidx++;
            if (--empty_visits == 0) return 1;
        }
        //找到一个非空的table节点
        de = d->ht[].table[d->rehashidx];
        //把链表上的节点逐个迁移
        while(de) {
            unsigned int h;

            nextde = de->next;
            h = dictHashKey(d, de->key) & d->ht[].sizemask;
            de->next = d->ht[].table[h];
            d->ht[].table[h] = de;
            d->ht[].used--;
            d->ht[].used++;
            de = nextde;
        }
        //清空原table节点
        d->ht[].table[d->rehashidx] = NULL;
        //迁移进度更新。
        d->rehashidx++;
    }

    //检查是否迁移完成
    if (d->ht[].used == ) {
        zfree(d->ht[].table);
        d->ht[] = d->ht[];
        _dictReset(&d->ht[]);
        d->rehashidx = -;
        return ;
    }

    return ;
}
           

可以看到,这个迁移数据是有数量限制的,只迁移一定数量的数据,如果没迁完就先放着。

同时再有这两个函数

//获取当前时间
long long timeInMilliseconds(void) {
    struct timeval tv;

    gettimeofday(&tv,NULL);
    return (((long long)tv.tv_sec)*)+(tv.tv_usec/);
}

int dictRehashMilliseconds(dict *d, int ms) {
    long long start = timeInMilliseconds();
    int rehashes = ;

    while(dictRehash(d,)) {
        rehashes += ;
        if (timeInMilliseconds()-start > ms) break;
    }
    return rehashes;
}
           

可以看出来这两个函数是用来控制迁移的时间的。如果hashtable过大,则一次性迁移完需要的时间实在太多,长时间处理会影响整个系统的性能。

redis的解决方案是蚂蚁搬家式的。只在dictRehash中大规模的迁移一次,然后在之后的每次增删查改中都摊派一次迁移工作,直到整个dictht迁移完成。

可以看到在dictAdd, dictDelete, dictFind, dictGetEandomKey等函数中都包含了

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

他们都会在执行过程中完成一组迁移。

那么,如何保证在迁移完成前的正确性呢?

正确性就是靠dict中的rehashidx字段来保证的,它记录了迁移的进度。

如果在增删查改中,发现涉及的key的hash指向table中的第i项,那么就会先拿i和rehashidx比较,看看table中第i项的链表已经是否被迁移了,如果被迁移了,则到ht[1]中寻找,如果没有迁移,则到ht[0]中寻找。

其他的增删查改没什么特别之处。

整个hashtable的实现似乎和JDK的非常相似。唯一的区别是这里的hastable不会在链表过长的时候自动转换为红黑树。

继续阅读