redis 字典

redis中的字典就跟正常语言中的hash差不多,同样需要计算key来索引,同样需要解决冲突问题等。
代码位于src/dict.c|h中

dictEntry结构体

字典的基本组成是dictEntry结构体:

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

其中key类型是void*,估计还是因为多态吧?后面再研究这个问题。
v是一个union,是具体的值;
为了解决键冲突,dictEntry本质是个单向链表,遇到冲突的时候,会把冲突键值对放在同一个索引的链表中,所以hash的查找不一定是O(1)。

dictht结构体

这是一个完整的字典,字典中的键值对存在这个结构体指定的地方。

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

首先,dictht结构体中有一个dictEntry数组,用来存储键值对,而每一个键值对是一个单向链表;
size 表示
dictEntry数组的大小;
sizemask是一个比size正好小1的数字,个人猜测是字典的操作中几乎每次都跟索引查询有关,所以单独保存这个值以提高效率,不知道是不是这样;
最后,used表示当前哈希表中已经有了多少个dictEntry,这个值与size一起构成哈希表进行扩缩容的重要指标。

dict结构体

有意思的地方在于上面的两个结构体都不是对外的,实际上字典对外的结构体是dict:

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    unsigned long iterators; /* number of iterators currently running */
} dict;

从这个结构体可以看到几个信息:

  1. 每个字典都有一个dictht二元数组,即ht成员,正常情况下字典的增删改查都在h[0]上进行,而h[1]大部分时候都是空指针;但当字典需要扩缩容时,即所谓的rehash,就要使用h[1],直到rehash完成,h[1]会重新置为空;
  2. 字典有一个rehashidx属性,这个属性用来标记字典当前是否在rehash;redis字典的rehash不是连续完成的,而是分阶段进行;正常情况下这个值都为-1表示没有进行rehash,当进入rehash状态时,这个值会从0开始计数,直到其值与ht[0]的size相同,此时rehash完成,值会重新变为-1;
  3. 字典有一个dictType指针成员,它的结构如下:
 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;

目测就是字典计算key的哈希值用的;

  1. 字典有一个privdata成员,目前还没有发现其作用,不过从代码上看,在计算key时会用到,具体使用可能得到redis的server中查看;
  2. 有一个iteratirs成员,对字典迭代时使用,具体使用需要继续研究;

重要属性

字典初始大小

#define DICT_HT_INITIAL_SIZE     4

这个标记了一个字典刚生成时的size;

dict_can_resize

static int dict_can_resize = 1;

这个值标记字典当前是否可以被rehash,1表示可以,0表示不行;不过作者也说了,即使为0也并不能完全阻止rehash,因为下面的另一个变量;

dict_force_resize_ratio

static unsigned int dict_force_resize_ratio = 5;

这个值是used与size的比值的阈值,当比值超过这个值时,即使上面的值设置为0,也是可以rehash的;

dict_hash_function_seed

static uint8_t dict_hash_function_seed[16];
  • 这个值似乎是当随机返回键值对时使用,可能有别的用处,尚待继续研究;

接口

创建新字典

dict *dictCreate(dictType *type,
        void *privDataPtr)
{
    dict *d = zmalloc(sizeof(*d));

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

压缩字典
int dictResize(dict *d)

{
    unsigned long minimal;

    if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;
    minimal = d->ht[0].used;
    if (minimal < DICT_HT_INITIAL_SIZE)
        minimal = DICT_HT_INITIAL_SIZE;
    return dictExpand(d, minimal);
}

虽然函数叫做dictResize,但从代码中可以看到,这里没有扩而只有缩,它会找到一个比当前size小但是又刚好比字典size大的第一个2的次方数,将字典的size设置为这个值,然后将字典resize;

扩大字典容量

int dictExpand(dict *d, unsigned long size)
{
    /* the size is invalid if it is smaller than the number of
     * elements already inside the hash table */
    if (dictIsRehashing(d) || d->ht[0].used > size)
        return DICT_ERR;

    dictht n; /* the new hash table */
    unsigned long realsize = _dictNextPower(size); // 找到第一个满足要求的2的次方幂;

    /* Rehashing to the same table size is not useful. */
    if (realsize == d->ht[0].size) return DICT_ERR;

    /* Allocate the new hash table and initialize all pointers to NULL */
    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;
    }

    /* Prepare a second hash table for incremental rehashing */
    d->ht[1] = n;
    d->rehashidx = 0;
    return DICT_OK;
}

这个是用来扩大字典容量的方法,需要注意的地方是它根据字典是初始化还是真正的rehash进行不同的操作;上面的dictCreate函数只是创建出dict和它的两个dictht成员,但是h[0]的table初始化时是空指针,因此这里就需要有个指向实际表的过程;

字典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];
        /* Move all the keys in this bucket from the old to the new hash HT */
        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++;
    }

    /* Check if we already rehashed the whole table... */
    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,虽然长,但过程很简单,就是不停的对h[0]中的每一项进行遍历,查看它是否为空,如果不为空,就对这个链表上的每个键值对重新哈希到h[1]上,哈希完毕后将h[0]指向h[1],而h[1]继续为空,指针就是好啊;
这里可以看到rehashidx的使用,需要注意几个地方:
1 rehashidx是有符号的,而size却是无符号的,所以断言时必须转换类型;
2 rehashidx这里显示了它的实际用处,那就是标记目前对表0rehash到了哪个位置,因为rehash是个不断迭代的过程,这里能看到,当迭代进行到一定时间时,就会被终止,而rehashidx记录了本次终止时table被执行到了哪个索引处,下次继续时就不需要从头开始了;另外由于做了断言,也不会发生溢出;

  • 有一个问题没有想明白,为什么这里会同时有n和empty_visits两个计数器,理论上只要有后者就够了,为什么前者也一直在循环?

在给定时间内执行rehash

int dictRehashMilliseconds(dict *d, int ms) {
    if (d->iterators > 0) return 0;

    long long start = timeInMilliseconds();
    int rehashes = 0;

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

字典插入键值对

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;

    if (dictIsRehashing(d)) _dictRehashStep(d); // 渐进式rehash,rehash永远进行时;

    /* Get the index of the new element, or -1 if
     * the element already exists. */
    if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)
        return NULL;

    /* Allocate the memory and store the new entry.
     * Insert the element in top, with the assumption that in a database
     * system it is more likely that recently added entries are accessed
     * more frequently. */
    ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
    entry = zmalloc(sizeof(*entry));
    entry->next = ht->table[index];
    ht->table[index] = entry;
    ht->used++;

    /* Set the hash entry fields. */
    dictSetKey(d, entry, key);
    return entry;
}

它会调用下面的dictAddRaw函数,它将键值对插入字典中,如果键已经存在,就更新相应的值;按照作者的意思,dictAddRaw也是一个用户可以直接调用的函数,它会查询key是否已经在字典中,如果key一ing存在,就将相应的dictEntry存储在existing中,这样的好处是用户一旦发现已经有键,可以直接更新值而不是什么都做不了或者还需要先删除再操作;需要记住,addRaw只有两种情况,如果键已经存在就返回空指针,但是existing中会存储键值对的指针;如果不存在就会新建一个键值对出来并将其指针返回;
还要注意的一点是,函数被调用时,它会判断字典当前是否处在rehash中,如果是,则会先执行一些rehash动作,很多函数中都有这个操作;

替换键值对的值

int dictReplace(dict *d, void *key, void *val)
{
    dictEntry *entry, *existing, auxentry;

    /* Try to add the element. If the key
     * does not exists dictAdd will succeed. */
    entry = dictAddRaw(d,key,&existing);
    if (entry) {
        dictSetVal(d, entry, val);
        return 1;
    }
    auxentry = *existing; // 从这里开始要注意设置新值以及删除旧值的顺序,不能颠倒;
    dictSetVal(d, existing, val);
    dictFreeVal(d, &auxentry);
    return 0;
}

这个方法会调用addRaw去查询键是否已经存在于字典中,注意上面提到的addRaw返回值的情况,如果返回非空,说明创建了一个新的键值对,此时只要设置值即可;如果返回空,说明要更新值,而地址存储在existing中;

添加新键值对或者查询key对应的键值对

dictEntry *dictAddOrFind(dict *d, void *key) {
    dictEntry *entry, *existing;
    entry = dictAddRaw(d,key,&existing);
    return entry ? entry : existing;
}

没什么,就是单纯调用addRaw,要不返回新添加的键值对,要不返回已经存在的键值对;

删除键值对

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;

    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)) {
                /* Unlink the element from the list */
                if (prevHe)
                    prevHe->next = he->next;
                else
                    d->ht[table].table[idx] = he->next;
                if (!nofree) {
                    dictFreeKey(d, he);
                    dictFreeVal(d, he);
                    zfree(he);
                }
                d->ht[table].used--;
                return he;
            }
            prevHe = he;
            he = he->next;
        }
        if (!dictIsRehashing(d)) break;
    }
    return NULL; /* not found */
}

查询键是否已经存在于字典中,如果查询到就将其从字典中移除,然后根据情况决定是否需要删除键和值;不过这个方法只是个对内的方法,是其它方法的base;

真正的删除键值对方法

int dictDelete(dict *ht, const void *key) {
    return dictGenericDelete(ht,key,0) ? DICT_OK : DICT_ERR;
}

调用上面这个函数去执行键值对的删除,这个函数设置的nofree是0,意味着它会将key和value的空间真正删除掉并忽略返回的键值对;之所以这么做,是因为有时候查找到键值对后需要立马执行删除,而有时候不需要,因此上面的dictGenericDelete函数就做了包了一层的动作;

unlink

dictEntry *dictUnlink(dict *ht, const void *key) {
    return dictGenericDelete(ht,key,1);
}

/* You need to call this function to really free the entry after a call
 * to dictUnlink(). It's safe to call this function with 'he' = NULL. */
void dictFreeUnlinkedEntry(dict *d, dictEntry *he) {
    if (he == NULL) return;
    dictFreeKey(d, he);
    dictFreeVal(d, he);
    zfree(he);
}

不太好翻译这个函数名,作者的意思是有时候查找到键值对后要删除,但是删除前会执行一些其它动作,执行完毕才删除,因此出现了上面的unlink函数,它返回那个查到的键值对,但步删除其中的项,而是先让用户执行一些操作,但是最后调用方必须记得调用下面的dictFreeUnlinkedEntry函数,否则怕是得内存泄漏,这两个函数应该是成双成对出现的;

清除整个字典

int _dictClear(dict *d, dictht *ht, void(callback)(void *)) {
    unsigned long i;

    /* Free all the elements */
    for (i = 0; i < ht->size && ht->used > 0; i++) {
        dictEntry *he, *nextHe;

        if (callback && (i & 65535) == 0) callback(d->privdata); //释放前执行回调函数

        if ((he = ht->table[i]) == NULL) continue;
        while(he) {
            nextHe = he->next;
            dictFreeKey(d, he);
            dictFreeVal(d, he);
            zfree(he);
            ht->used--;
            he = nextHe;
        }
    }
    /* Free the table and the allocated cache structure */
    zfree(ht->table);
    /* Re-initialize the table */
    _dictReset(ht);
    return DICT_OK; /* never fails */
}

/* Clear & Release the hash table */
void dictRelease(dict *d)
{
    _dictClear(d,&d->ht[0],NULL);
    _dictClear(d,&d->ht[1],NULL);
    zfree(d);
}

_dictClear函数将字典的指定的table置为空,释放所有键值对的空间;
dictRelease则通过调用_dictClear函数的方式释放掉两个表,然后再释放掉dict本身占用的空间;

查找键是否已经存在于字典中以及键对应的值

dictEntry *dictFind(dict *d, const void *key)
{
    dictEntry *he;
    uint64_t h, idx, table;

    if (dictSize(d) == 0) return NULL; /* dict is empty */
    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];
        while(he) {
            if (key==he->key || dictCompareKeys(d, key, he->key))
                return he;
            he = he->next;
        }
        if (!dictIsRehashing(d)) return NULL;
    }
    return NULL;
}
void *dictFetchValue(dict *d, const void *key) {
    dictEntry *he;

    he = dictFind(d,key);
    return he ? dictGetVal(he) : NULL;
}

从字典中随机返回一个键

dictEntry *dictGetRandomKey(dict *d)
{
    dictEntry *he, *orighe;
    unsigned long h;
    int listlen, listele;

    if (dictSize(d) == 0) return NULL;
    if (dictIsRehashing(d)) _dictRehashStep(d);
    if (dictIsRehashing(d)) {
        do {
            /* We are sure there are no elements in indexes from 0
             * to rehashidx-1 */
            h = d->rehashidx + (random() % (dictSlots(d) - d->rehashidx));// 保证键从有效的区间开始算起
            he = (h >= d->ht[0].size) ? d->ht[1].table[h - d->ht[0].size] :
                                      d->ht[0].table[h];
        } while(he == NULL);
    } else {
        do {
            h = random() & d->ht[0].sizemask;
            he = d->ht[0].table[h];
        } while(he == NULL);
    }
    listlen = 0;
    orighe = he;
    while(he) {
        he = he->next;
        listlen++;
    }
    listele = random() % listlen;
    he = orighe;
    while(listele--) he = he->next;
    return he;
}

唯一需要注意的是当处在rehash模式时,因为rehashidx记录了表0中已经被清空的索引,所以获取键时可以直接跳过这些区间,然后在表0和表1中剩下的所有区间中去随机返回,其中的dictSlots是一个宏定义,用来计算两个表当前总的size的大小;

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值