redis字典源码简单分析

字典是redis底层数据结构之一,在dict.c中实现,下面分析下他的实现。

一.简介

redis的dict仍然是一个数组+链表实现的哈希表。区别为redis的dict有两个哈希表,第二个哈希表resize时使用,如下为它头文件中定义的结构:

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

typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    //当前哈希表中
    unsigned long used;
} dictht;

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx; //rehash进度
    unsigned long iterators; /* number of iterators currently running */
} dict;

redis的字典有两个哈希表dictht[0]和dictht[1],只有rehash时两个哈希表才同时使用,平时只有dictht[0]哈希表中有数据。
rehashidx用来标记rehash正在执行到哪个槽,如果没有rehashing则返回-1.
redis rehash最大的特点是渐进性rehash,将rehash操作分散在插入,删除等操作中。这点和ThreadPoolExecutor实现思想很像有木有。

二.实现分析:
1.插入:

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;
}

dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{
    long index;
    dictEntry *entry;
    dictht *ht;
    
    //如果正在rehashing,则执行一次_dictRehashStep操作。
    if (dictIsRehashing(d)) _dictRehashStep(d);

    //获取插入的key在数组的中的位置
    if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)
        return NULL;

    //如果正在执行rehash操作则将entry插入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;
    //负载+1
    ht->used++;

    dictSetKey(d, entry, key);
    return entry;
}

//返回一个key所在的数组下标并判断该key是否存在。如果没有执行rehash,则只搜索ht[0]并返回该key在ht[0]的下标。如果正在rehash则两个哈希表都搜索并返回在ht[1]的下标.
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;
    
    //先搜索第一个哈希表,搜索完第一个哈希表之后,如果当前字典没有在执行rehash,则不搜索第二个哈希表。如果正在执行rehash,则在搜索第二个哈希表。
    for (table = 0; table <= 1; table++) {
        //取余计算key应放置的数组下标
        idx = hash & d->ht[table].sizemask;
        //沿着当前槽的链表向后搜索是否有该key,如果有返回-1.
        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;
        }
        //如果没有rehashing,则不再搜索ht[1]
        if (!dictIsRehashing(d)) break;
    }
    return idx;
}

//检查当前哈希表是否需要扩容,正在rehash的话不扩容,扩容后大小为比used*2大的最接近的2的n次幂。
static int _dictExpandIfNeeded(dict *d)
{
    //正在rehash的话不扩容。
    if (dictIsRehashing(d)) return DICT_OK;

    //如果ht[0]是空,则初始化
    if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);
    
    //负载因为为1.但是如果正在执行bgsave操作,负载因子为dict_force_resize_ratio 5
    if (d->ht[0].used >= d->ht[0].size &&
        (dict_can_resize ||
         d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
    {
        //扩容,扩容数为当前已有元素数量*2
        return dictExpand(d, d->ht[0].used*2);
    }
    return DICT_OK;
}

//扩容
int dictExpand(dict *d, unsigned long size)
{
    //如果正在rehashing或者扩容之后负载仍然大于1,返回错误
    if (dictIsRehashing(d) || d->ht[0].used > size)
        return DICT_ERR;

    dictht n; /* the new hash table */
    //确保size为2的倍数
    unsigned long realsize = _dictNextPower(size);

    //如果扩容之后的size仍等于ht[0]size,则返回错误。
    if (realsize == d->ht[0].size) return DICT_ERR;

    //开辟哈希表空间
    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;
    }

    //将新建的哈希表赋值给ht[1]
    d->ht[1] = n;
    d->rehashidx = 0;
    return DICT_OK;
}

插入操作有几个要点:
1.渐进性hash:
插入时会判断当前字典是否正在rehashing,如果正在rehashing,会执行一次rehash操作.
2.二倍扩容
扩容后大小为比used*2大的最接近的2的n次幂。这样&运算即可算出key要插入的数组下标。
3.负载因子为1
但是如果dict_can_resize标志位为true,表明正在执行rdb写入操作,此时负载因子为5.
4.头插法
新的节点插入到表头
5.如果正在rehash,则新节点直接插入到ht[1]。

rehash:

int dictRehash(dict *d, int n) {
    int empty_visits = n*10; /* Max number of empty buckets to visit. */
    if (!dictIsRehashing(d)) return 0;

    while(n-- && d->ht[0].used != 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;
        }
        de = d->ht[0].table[d->rehashidx];
        //将当前槽上链表的所有节点都引动到ht[1]
        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++;
    }

    //检查时候完成,完成的话将整个ht[1]赋值给ht[0].
    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操作很简单,一次rehash操作移动一个槽。rehash完成后会释放ht[0],并将ht[1]赋值给ht[0].

你可能感兴趣的:(redis字典源码简单分析)