redis源码分析之五基础的数据结构字典

一、dict 字典

在Redis中,字典就是HASH表。哈希表的优势在于查找速度快(理想状态下O(1)),但大小不好控制,大了浪费,小了冲突。而过多的冲突最终会使得哈希表退化。这就需要有一个处理机制,来达到容量和冲突解决的一个动态平衡。在Redis中,字典可以自动动态扩容,为了保证适应性和安全性,DICT不是一次完成扩容的,是渐进的,批次完成的。

二、源码分析

1、字典的定义:

//字典的节点定义
typedef struct dictEntry {
    void * key;//这个好理解,KEY
    union {
        void * val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;//KEY对应的值
    struct dictEntry * next;//冲突节点链表的下一个指针
} dictEntry;
//哈希表
typedef struct dictht {
    dictEntry ** table;//二级指针,意思是一个dictEntry指针数组
    unsigned long size;//table的总大小
    unsigned long sizemask;//取余的大小---C数组从0开始
    unsigned long used;//现有节点数量
} dictht;
//dict的定义
typedef struct dict {
    dictType * type;
    void * privdata;//私有数据,参数,源码中基本为NULL
    //哈希表---两个供扩容
    dictht ht[2];
    //Rehash当前的状态
    long rehashidx; /* rehashing not in progress if rehashidx == -1 * /
    unsigned long iterators; /* number of iterators currently running * /
} dict;
//字典类型中用到的处理函数
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);//key value释放函数
    void (*valDestructor)(void * privdata, void * obj);
} dictType;

如果简单的只是提供一些基础类型的字典,那么dictType的意义并不多大,同样,如果HASH足够大和数量相对少,struct dictEntry * next也没有什么意义。前者增加处理的各种函数,就提供了针对更多类型的复制、比较和资源释放的支持。而后者则解决了HASH的冲突的问题(哈希表的碰撞解决方法,有兴趣可以到网上查找一下)。

2、字典生成的过程
当在命令中使用HSET设置数据时,首先根据dictType的的hashFunction(redis中提供了不少相关的计算函数),计算HASH值(#define dictHashKey(d, key) (d)->type->hashFunction(key)),然后通过上面提到的sizemask(index = dictHashKey(d, key) & d->ht[x].sizemask;),计算得到索引位置。
这时候有两种情况,假如索引位置为空,直接插入;否则,就是next大展神威的时候儿了。看一看相关的源码:
创建一个DICT:

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[0]);
    _dictReset(&d->ht[1]);
    d->type = type;
    d->privdata = privDataPtr;
    d->rehashidx = -1;
    d->iterators = 0;
    return DICT_OK;
}

3、插入:

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;

    //判断是滞正在Rehash,因为插入过程需要同时判断是否需要Rehash
    //如果在进行,则进行桶的迁移
    if (dictIsRehashing(d)) _dictRehashStep(d);

    /* Get the index of the new element, or -1 if
     * the element already exists. * /
     //获得索引,如果返回-1表示已经存在,需要处理冲突
    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. * /
     //判断是否在Rehash,如果是,则插入HT[1]
    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. * /
    //K/V映射
    dictSetKey(d, entry, key);
    return entry;
}
static long _dictKeyIndex(dict *d, const void *key, uint64_t hash, dictEntry **existing)
{
    unsigned long idx, table;
    dictEntry *he;
    if (existing) *existing = NULL;

    /* Expand the hash table if needed */
    if (_dictExpandIfNeeded(d) == DICT_ERR)
        return -1;
    for (table = 0; table <= 1; table++) {
        idx = hash & d->ht[table].sizemask;
        /* Search if this slot does not already contain the given key * /
        //遍历KEY是否存在
        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;
        }
        if (!dictIsRehashing(d)) break;
    }
    return idx;
}

static int _dictExpandIfNeeded(dict *d)
{
    /* Incremental rehashing already in progress. Return. */
    //如果正在Rehash,直接返回
    if (dictIsRehashing(d)) return DICT_OK;

    /* If the hash table is empty expand it to the initial size. */
    if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);

    /* If we reached the 1:1 ratio, and we are allowed to resize the hash
     * table (global setting) or we should avoid it but the ratio between
     * elements/buckets is over the "safe" threshold, we resize doubling
     * the number of buckets. */
    if (d->ht[0].used >= d->ht[0].size &&
        (dict_can_resize ||
         d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
    {
      //扩展的标准
        return dictExpand(d, d->ht[0].used* 2);
    }
    return DICT_OK;
}
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 */
    //看名称也知道是2的幂次增长
    unsigned long realsize = _dictNextPower(size);

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

4、rehash过程

//返回0表示迁移完成,返回1表示仍然需要移动数据
//迁移的步骤是以哈希表作为单位的,需要注意的是,这个表可能有多个节点,类似于桶:
int dictRehash(dict *d, int n) {
    int empty_visits = n*10; /* Max number of empty buckets to visit. */
    //是否正在进行Rehash
    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 * /
        //HT[0]为空,则表示Rehash完成
        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 * /
        //迁移0到1
        while(de) {
            uint64_t h;

            nextde = de->next;//存储下一个节点的指针
            /* Get the index in the new hash table * /
            //计算HASH值及索引位置
            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) {
      //释放0并将1设置为0,并重置HT[1]
        zfree(d->ht[0].table);
        d->ht[0] = d->ht[1];
        _dictReset(&d->ht[1]);
        d->rehashidx = -1;//设置完成Rehash
        return 0;
    }

    /* More to rehash... */
    return 1;
}

在Rehash时如果遇到操作运作,则同时会在HT[0],HT[1]上进行操作。例如在HT[0]上查找没找到,就会去HT[1]上查找。Rehash步骤如下:
将ht[1]哈希表扩容,扩容标准为如下操作:
扩展操作,那么ht[1]的容量为第一个大于等于等于ht[0].used*2的2n;收缩操作,那么ht[1]的容量为第一个大于等于ht[0].used的2n;
将ht[0]中的数据迁移ht[1],重新计算HASH和索引。
释放HT[0],并将HT[1]复制到HT[0],并重置HT[1]。
需要注意的正如注释中所说,迁移是分步进行的,不是一次完成的。
哈希表的扩容在前面提到过是自动进行的,它有两个判断标准:
服务器目前没有执行BGSAVE或BGREWRITEAOF命令,并且哈希表负载因子大于等于1。
服务器正在执行BGSAVE或BGREWRITEAOF命令,并且哈希表负载因子大于等于5。
当负载因子小于0.1时,自动进行收缩操作。
在上面的插入操作中可以看到,会进行Rehash操作,同样,在删除和查找等时,也会如此,这样通过对rehashidx索引的判断,来最终确定是否完成了Rehash,将一个全量的操作,分解成了几步的操作,减轻了整个服务器进程的压力,同时也降低了操作的风险。

三、总结

通过上述的分析,基本明白了字典的工作的框架,后面的删除和查找都没有解析,因为基本都是这样的原理,可以对着源码分析一下即可。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值