redis5.0源码浅析3-字典dict

1.结构体分析

字典dict数据结构 实际上是哈希表。dictEntry 是链表中的节点
节点结构体dictEntry 
typedef struct dictEntry {
    void *key;// key指针
    union {// val 联合体 value可能是不同类型的
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;//链表指针 指向下个节点
} dictEntry;

知识点
c语言中union及struct区别

union(联合体)使用类似于struct(结构体),但是它与struct有如下明显差异:
1.union中可以定义多个成员变量,union的大小由最大的成员的大小决定。struct同样可由多个数据类型成员组成,struct的大小也是所有成员的大小之和(考虑对齐)。
2.union一次只能使用其中一个成员,所有成员共享同一块内存。当对其它成员进行赋值,原来成员的值就不存在了。而struct成员的赋值是不会互相影响的。
3.union只能用其第一个成员类型的值进行初始化。
在dictEntry 中用了结构体 是因为value可能是数值或其他类型


类型结构体,保存各种函数变量
typedef struct dictType {
    unsigned int (*hashFunction)(const void *key);//获取key的哈希值函数变量
    void *(*keyDup)(void *privdata, const void *key);复制key的函数变量
    void *(*valDup)(void *privdata, const void *obj);//复制value的函数变量
    int (*keyCompare)(void *privdata, const void *key1, const void *key2);//key比较的函数变量
    void (*keyDestructor)(void *privdata, void *key);//key释放内存的 函数变量
    void (*valDestructor)(void *privdata, void *obj);//value释放内存的 函数变量
} dictType;

哈希表结构体dictht 
typedef struct dictht {
    dictEntry **table;/哈希表
    dictType *type;
    unsigned long size;//哈希表长度
    unsigned long sizemask;//掩码用于计算索引值 总是等于 size-1
    unsigned long used;//已有节点数量
} dictht;

字典结构体dict
typedef struct dict {
    dictType *type;//类型结构体,保存各种函数变量
    void *privdata;
    dictht ht[2];//2个哈希表
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    unsigned long iterators; /* number of iterators currently running */
} dict;

2.初始化

2.1 创建dict-dictCreate


//dictCreate只是初始化了结构体,没有为ht[0]->table,ht[1]->table 申请空间。
/* Create a new hash table */
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;
}
static void _dictReset(dictht *ht)
{
    ht->table = NULL;
    ht->size = 0;
    ht->sizemask = 0;
    ht->used = 0;
}
为dict结构申请内存,初始的ht[0]和ht[1] table=null,size=0,sizemask=0,used=0.即调用dictCreate时没有初始化table空间。在调用dictAdd的时候进行扩展。

3.添加节点

3.1 dictAddRaw函数

//dict add方法 依赖dictAddRaw
int dictAdd(dict *d, void *key, void *val)
{
    dictEntry *entry = dictAddRaw(d,key,NULL);

    if (!entry) return DICT_ERR;
    dictSetVal(d, entry, val);// set val
    return DICT_OK;
}

//添加新的key 节点,如果成功返回新的 dictEntry,如果key节点 已经存在返回NULL,并且把存在的节点地址赋值给 existing 
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{
    long index;
    dictEntry *entry;
    dictht *ht;
// rehash进行中,执行渐进式哈希,每次处理一个槽位。
    if (dictIsRehashing(d)) _dictRehashStep(d);// code 1


// 获取新节点的index,如果key节点已经存在返回-1,根据需要执行扩展,  如果key节点 已经存在 且existing 不等于null,将key节点指针赋值给existing ._dictKeyIndex 包含hash表扩展的处理
    if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)//code 2
        return NULL; // index==-1说明节点已经存

//申请内存和插入新实例。
// rehash进行中使用ht[1],rehash不在进行中使用ht[0]
    ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
    entry = zmalloc(sizeof(*entry));
//将新实例放在 index 链表头
    entry->next = ht->table[index];
    ht->table[index] = entry;
    ht->used++;

    /* Set the hash entry fields. */
    dictSetKey(d, entry, key);// 如果有传值函数 执行传值函数,没有 通过指针赋值。
    return entry;
}
dictAddRaw 中插入节点的部分是比较容易理解的, rehash部分即code 1 和 hash表扩展部分 即code2 是比较难理解的,下面详细分析

先分析 比较简单的  hash表扩展部分 后分析 rehash
 

3.2 hash表扩展

_dictKeyIndex 包含hash表扩展的处理,先分析一下它
//根据需要执行扩展,查找key是否存在,如果存在返回-1。
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 */
  //执行dict ht[0]初始化,为后续rehash做准备。
    if (_dictExpandIfNeeded(d) == DICT_ERR)
        return -1;
//查找key是否存在, 遍历 ht[0]->table 和ht[1]->table
    for (table = 0; table <= 1; table++) {
//计算index
        idx = hash & d->ht[table].sizemask;
        /* Search if this slot does not already contain the given key */
//获取 index指向的第一个地址
        he = d->ht[table].table[idx];
//he指向一个链表结构,从he的头到尾查找。
        while(he) {
            if (key==he->key || dictCompareKeys(d, key, he->key)) {
                if (existing) *existing = he;
                return -1;
            }
            he = he->next;
        }
//rehash不在进行中,则只在 ht[0]查找,需要执行break. rehash在进行中 ,ht[0] h[1] 都需要查找
        if (!dictIsRehashing(d)) break;
    }
    return idx;
}


//如果hash table 不是空的,使用不同的size进行扩展
static int _dictExpandIfNeeded(dict *d)
{

//如果扩展进行中,返回成功
    if (dictIsRehashing(d)) return DICT_OK;
//如果hash table 是空的,则进行初始化扩展
    /* 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);// 初始size==4
//used 与size 的比例 叫做负载因子比例
//负载因子达到1:1比例 且 允许扩展或超过了强制扩展比例,执行 2倍扩容
    if (d->ht[0].used >= d->ht[0].size &&
//dict_force_resize_ratio默认是5 可以通过参数设置
        (dict_can_resize ||
         d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
    {// dict_can_resize 默认 1 ,说明  当负载因子比例 到1 就会执行 dictExpand
        return dictExpand(d, d->ht[0].used*2);
    }
    return DICT_OK;
}
重点:used 与size 的比例 叫做负载因子比例,负载因子达到1:1比例。执行 2倍扩容

_dictExpandIfNeeded 在初始化和 当负载因子比例大于等于1 时都会执行dictExpand
dictht 是一个 哈希表结构。

/* Expand or create the hash table */
//扩展内存 ,如果是初始化则将ht[0]指向新的dictht ,否则进行rehash准备处理.
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 */  // n 没有申请动态内存, 说明结构体 可以直接使用其基本类型部分
//获取大于等于size的2的倍数的最小值  初始化 hash table 时 size=4,下一次扩展2倍
    unsigned long realsize = _dictNextPower(size);

    /* Rehashing to the same table size is not useful. */
//校验resize(此处情况不可能发生)
    if (realsize == d->ht[0].size) return DICT_ERR;

    /* Allocate the new hash table and initialize all pointers to NULL */
// new  hash table分配内存,
    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. */
//如果是第一次初始化扩展,将ht[0]指向 新创建的dictht 
    if (d->ht[0].table == NULL) {
        d->ht[0] = n;
        return DICT_OK;
    }

    /* Prepare a second hash table for incremental rehashing */
//如果不是第一次初始化扩展,执行rehash准备处理, 将ht[1]指向  dictht ,标识 rehash进行中.
    d->ht[1] = n;
    d->rehashidx = 0;
    return DICT_OK;
}
dictExpand主要做2件事

  1. 执行dict ht[0]初始化
  2. 使用新的size创建新的dictht,  ht[1]指向这个dictht, 标识rehash为正在进行中, 为后续rehash做准备。

注意:dictExpand 没有执行  rehash ,只是执行 h[1]初始化, d->rehashidx = 0;标识 rehash进行中。


3.3 rehash流程分析

redis dict hash 原来使用MurmurHash算法 ,因为MurmurHash有漏洞使用SipHash

rehash是什么时候执行的呢

例如:dicAddRaw
    if (dictIsRehashing(d)) _dictRehashStep(d);// code 1
#define dictIsRehashing(d) ((d)->rehashidx != -1) 
当(d)->rehashidx != -1 时 执行rehash
通过跟踪 dictFind,dictAddRaw ,dictGenericDelete,都有调用,也就是说 查找,添加,删除都会执行rehash

rehashidx是什么时候修改的?

执行dictExpand时将 rehashidx修改为0
调用链如图,可知主要的是 add时候 执行修改 rehashidx.

3.4 _dictRehashStep函数

//渐进式rehash 依赖dictRehash, 参数=1, 每次处理一个槽位
static void _dictRehashStep(dict *d) {
    if (d->iterators == 0) dictRehash(d,1);//当没有执行迭代遍历的时候 才能执行rehash
}
//rehash真正实现 ,每次处理n个槽位(一个槽位对应一条链表,槽位有可能为null)
int dictRehash(dict *d, int n) {
    int empty_visits = n*10; /* Max number of empty buckets to visit. */
    if (!dictIsRehashing(d)) return 0;
//从0~(d.size-1)每次循环处理一个槽位上的链表
    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);
//跳过空槽位,连续空槽位的的数量超过n的10倍,返回1
        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 */
//移动当前槽位的所有key到新的 ht上
        while(de) {
//逐个移动槽位链表上的entry
            uint64_t h;

            nextde = de->next;
            /* Get the index in the new hash table */
//获取新 hashtable上的槽位 index
            h = dictHashKey(d, de->key) & d->ht[1].sizemask;
//将de插入ht[1].table[h]的头部位置。 与添加不同 rehash要插入到槽位的头部。
            de->next = d->ht[1].table[h];
            d->ht[1].table[h] = de;
            d->ht[0].used--;
            d->ht[1].used++;
            de = nextde;
        }
//修改原来的槽位指向NULL
        d->ht[0].table[d->rehashidx] = NULL;
//槽位索引加1,如果完成rehash,rehasdix=d.size-1
        d->rehashidx++;
    }

    /* Check if we already rehashed the whole table... */
//检查是否已经完成rehash修改 标识
    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;
}


dictRehash 在添加和查询的时候都调用
调用链如下图

dict add的逻辑比较复杂 整理出下图


实现add的过程中 解决问题的思路是什么

整体思路
add的插入实例部分实现比较常规,没什么可说的,执行add dict空间可能不够,所以预先检查dict空间。如果不够则进行扩展。
由于一次执行完整个dict的rehash比较耗时,采用了rehash的方案,将rehash分散在dict添加,修改,删除中。

代码细节上的思路
第一次add 可以执行 初始化空间扩展
检查key是否存在的同时,检查空间是否足够,不够则扩展

总结:添加节点 包含了 rehash,dict扩展,插入节点三部分处理。  插入节点的逻辑比较简单,
重点是 理解rehash,dict扩展。

4.替换和遍历

4.1 替换

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. */
//如果不存在则写入,如果存在 使用existing获取 实例地址
    entry = dictAddRaw(d,key,&existing);
    if (entry) {
        dictSetVal(d, entry, val);
        return 1;
    }


// 赋值新值释放旧值,注意这么做很重要,因为 旧值和新值可能是同一个引用,必须先增加引用,再减少引用
    auxentry = *existing;
    dictSetVal(d, existing, val);
    dictFreeVal(d, &auxentry);// 如果配置了 val释放函数 ,执行它。
    return 0;
}

4.2 遍历

遍历 dict 代码如下
dictIterator * iter=dictGetIterator(dict);
dictEntry *entry;
while((entry=dictNext(iter))!=null){
void * key ,*val
key=entry->key;
 val=entry.v->val;
}
dictGetIterator 初始化了dictIterator ,代码如下

dictIterator *dictGetIterator(dict *d)
{
    dictIterator *iter = zmalloc(sizeof(*iter));

    iter->d = d;
    iter->table = 0;
    iter->index = -1;
    iter->safe = 0;
    iter->entry = NULL;
    iter->nextEntry = NULL;
    return iter;
}

//找到下个节点
// index 是table[n] 中的索引,table[1]或table[2]称作一个槽位, iter->entry是 index槽位上的链表中某个节点 
dictEntry *dictNext(dictIterator *iter)
{
//当iter->entry == NULL 时,第一次执行dictNext 或 index所属槽位上的节点已经轮询完毕,
//此时iter->index++; iter->entry=ht->table[iter->index]   iter->entry 指向了 下个槽位的头部位置,也就是头部第一个节点的位置。
    while (1) {
        if (iter->entry == NULL) {
            dictht *ht = &iter->d->ht[iter->table];

            if (iter->index == -1 && iter->table == 0) {
//iter初始化后执行此块
                if (iter->safe)
                    iter->d->iterators++;
                else
                    iter->fingerprint = dictFingerprint(iter->d);
            }
            iter->index++;
            if (iter->index >= (long) ht->size) {
//如果dict[0]已经迭代获取完毕, dict正在进行rehash,则使用ht[1]迭代获取
                if (dictIsRehashing(iter->d) && iter->table == 0) {
                    iter->table++;
                    iter->index = 0;
                    ht = &iter->d->ht[1];
                } else {
                    break;
                }
            }
//iter->entry 指向了 下个槽位的头部位置,也就是头部第一个接单的位置。
            iter->entry = ht->table[iter->index];
        } else {
//当前链表未耗尽,继续取出next
            iter->entry = iter->nextEntry;
        }
//如果iter->entry!=NULL,记录 next节点。
//如果iter->entry==NULL,执行下轮循环 查找 下个槽位的开始节点
        if (iter->entry) {
            /* We need to save the 'next' here, the iterator user
             * may delete the entry we are returning. */
            iter->nextEntry = iter->entry->next;
            return iter->entry;
        }
    }
    return NULL;
}

逻辑总结:
如果当前 entry指向的节点是NULL,说明第一次执行dictNext 或 index所属槽位上的节点已经轮询完毕, index++获取一个槽位开始节点, 否则沿着当前的链表 向后遍历, 最后返回遍历的节点。

 

 

 

 

 

 

 

 

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值