redis6.0源码字典的实现

redis的字典使用哈希表作为底层实现。

字典操作

数据结构定义

结构体定义在dict.h中。
字典的结构体定义:

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;

ht是一个长度为2的数组,对应的是两个哈希表,一般使用使用ht[0],ht[1]主要在扩容和缩容时使用。

哈希表结构体定义:

typedef struct dictht {
	//哈希表数组
    dictEntry **table;
    //哈希表大小
    unsigned long size;
   	//哈希表大小的掩码,用于计算索引值
   	//总是等于size-1
    unsigned long sizemask;
   	//已有节点的数量
    unsigned long used;
} dictht;

table是一个数组,对应的是多个哈希表节点dictEntry

哈希表节点key/value结构体定义:

typedef struct dictEntry {
	//键
    void *key;
    //值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    //指向下一个哈希表节点,形成链表
    struct dictEntry *next;
} dictEntry;

哈希表每个节点都保存着一个键值对,key就是键值对的键,v属性就是对应键值对的值,v可以是一个指针也可以是uint64_t,整数也可以是int64_t整数。
next是一个链表,指向着下一个哈希表节点,这个指针可以将多个哈希值相同的键值对连接在一起,以此来解决哈希冲突问题。
在这里插入图片描述

哈希算法

redis字典实现是使用链地址法,哈希算法具体方式为:

  1. 计算键key的hash值

    hash = (ht)->type->hashFunction(key)
    
  2. 通过hash与sizemask的位运算计算出哈希table数组(桶)对应的索引

    h = dictHashKey(ht, key) & ht->sizemask;
    
  3. 插入的时候,可能会出现键被分配到同一个哈希表数组的索引上,引发哈希冲突,解决哈希冲突的方式是每次把新增节点往单向链表的头部插入,每个节点会记录下一个节点的信息next。

  4. 每次插入的时候,会检查是否需要扩容,扩容由负载因子决定的,负载因子=哈希表已保存的节点数/哈希表大小

    d->ht[0].used/d->ht[0].size
    
  5. 扩容和收缩通过dictExpand完成,扩容或收缩表需要把ht[0]的所有键rehash到ht[1]里面,考虑服务器性能原因rehash并不会一次性地把所有ht[0]所有键rehash到ht[1]里,而是渐进式、分多次的完成。

字典的创建

dict *dictCreate(dictType *type,
        void *privDataPtr)
{	
	//分配创建字典的内存
    dict *d = zmalloc(sizeof(*d));
	//对字典进行初始化
    _dictInit(d,type,privDataPtr);
    return d;
}


int _dictInit(dict *d, dictType *type,
        void *privDataPtr)
{
	//初始化hash表
    _dictReset(&d->ht[0]);
    _dictReset(&d->ht[1]);
    d->type = type;
    d->privdata = privDataPtr;
    d->rehashidx = -1;
    d->iterators = 0;
    return DICT_OK;
}

字典扩容和收缩

在添加、释放字典的时候会调用_dictExpandIfNeeded函数判断字典是否需要扩容和收缩,扩容为二倍扩容,具体的扩容和收缩函数为dictExpand。

static int dictExpand(dict *ht, unsigned long size) {
	
    if (dictIsRehashing(d) || d->ht[0].used > size)
        return DICT_ERR;
	//新hash表
    dictht n; /* the new hash table */
    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;

   
    if (d->ht[0].table == NULL) {
        d->ht[0] = n;
        return DICT_OK;
    }
	
    d->ht[1] = n;
    d->rehashidx = 0;
    return DICT_OK;
}
  1. 创建一个新的哈希表
  2. 如果字典的0号hash表为空,则可能是第一次初始化,那么设置新哈希表为0号哈希表
  3. 如果字典的0号hash表不为空,则是扩容或收缩哈希表,那么设置新哈希表为1号哈希表,并更改rehashidx=0,表示正在rehash状态,rehashidx默认情况下为-1。

渐近式rehash

扩展和缩容表实际上不是一次完成的,因为考虑键值对可能非常大,如果一次完成可能会非常消耗性能。

渐进触发rehash

  • 在查找、添加等更新操作的时候会触发单步rehash,单步rehash表示会rehash一次某个不为空的桶上的所有链表节点。
  • 如果字典在rehash状态中,但是如果服务器长时间没执行命令,字典rehash就没办法完成,为了避免这种情况,redis会尝试花费1毫秒时间去执行100步主动rehash,具体执行函数incrementallyRehash

渐进式rehash方法在dict.c/dictRehash中

static void _dictRehashStep(dict *d) {
    if (d->iterators == 0) dictRehash(d,1);
}
int dictRehash(dict *d, int n) {
    int empty_visits = n*10; /* Max number of empty buckets to visit. */
    if (!dictIsRehashing(d)) return 0;
	//进行n步rehash
    while(n-- && d->ht[0].used != 0) {
        dictEntry *de, *nextde;
        //防止rehashidx越界
        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];
       
        while(de) {
            uint64_t h;

            nextde = de->next;
          
            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++;
    }

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

    return 1;
}
  1. 从ht[0]根据rehashidx遍历,获得ht[0]表中的哈希节点信息
  2. 每次遍历出哈希节点信息就把它迁移到ht[1]
  3. 如果ht[0]中没有hash节点,说明转移已经完成,把ht[1]设置成ht[0],并重置ht[1]

因为rehash是渐进式的,所以也会引发问题是在删除、查找、更新等操作的时候都在两个哈希表中进行。
另外在rehash期间,新添加的字典的键值对会保存到ht[1]里面,而ht[0]则不会进行任何添加操作,保证ht[0]只减不增,随着rehash操作执行最终变为空表。

添加元素到字典

添加的方法在dict.c/dictAdd

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

dictAdd会调用dictAddRaw生成一个新的哈希节点,然后调用dictSetVal去设置这个哈希节点的val值,设置哈希节点的key值是在dictAddRaw方法里。

dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{
    long index;
    dictEntry *entry;
    dictht *ht;
	//如果正在rehash,进行渐进rehash
    if (dictIsRehashing(d)) _dictRehashStep(d);

    if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)
        return NULL;
    ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
    entry = zmalloc(sizeof(*entry));
    entry->next = ht->table[index];
    ht->table[index] = entry;
    ht->used++;
    
    dictSetKey(d, entry, key);
    return entry;
}
  1. 获取哈希表对应的索引index
  2. 根据是否rehash状态获取ht[1]还是获取ht[0],如果正在refash,则用哈希表ht[1]进行插入
  3. 分配内存生成新的hash节点,然后在链表头部插入这个hash节点
  4. 更新对应hash节点信息

查找table的索引方法是在dict.c/_dictKeyIndex方法

static long _dictKeyIndex(dict *d, const void *key, uint64_t hash, dictEntry **existing)
{
	...
	 for (table = 0; table <= 1; table++) {
		idx = hash & d->ht[table].sizemask;
		//获取到哈希表节点的链表
		 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;
        }
	}
	...
	return idx;
}
  1. 分别从字典得ht[0]、ht[1]中查找
  2. 计算哈希表dictht中table的索引,计算方式为:idx = hash & sizemask,sizemask总会比size小1。
  3. 根据table的索引得到hash表节点,然后去遍历链表。
  4. 如果key不存在,返回table的索引,如果存在返回-1,并把节点信息通过existing返回。

字典元素替换

int dictReplace(dict *d, void *key, void *val)
{
    dictEntry *entry, *existing, auxentry;
    
    entry = dictAddRaw(d,key,&existing);
    if (entry) {
        dictSetVal(d, entry, val);
        return 1;
    }

    auxentry = *existing;
    dictSetVal(d, existing, val);
    dictFreeVal(d, &auxentry);
    return 0;
}
  1. 调用dictAddRaw添加hash节点,如果节点已经存在则entry会返回null,并把已存在节点返回existing
  2. 如果不存在,则直接添加,并返回1
  3. 如果不存在,修改节点的值

查找给定键的值

void *dictFetchValue(dict *d, const void *key) {
    dictEntry *he;

    he = dictFind(d,key);
    return he ? dictGetVal(he) : NULL;
}
dictEntry *dictFind(dict *d, const void *key)
{
    dictEntry *he;
    uint64_t h, idx, table;

    if (d->ht[0].used + d->ht[1].used == 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;
}
  1. 计算指定key的hash值
  2. 遍历hash表,根据hash值和sizemask计算出table的索引
  3. 取得hash节点链表信息进行遍历
  4. 找到节点信息后返回

迭代器

用字典生成迭代器

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

迭代器遍历字典

实际上遍历字典,因为最底层字典是链表存储的,所以遍历字典很简单相当于遍历字典就可以了,只是因为收缩和扩容的原因,需要遍历2个hash表。

dictEntry *dictNext(dictIterator *iter)
{
    while (1) {
        if (iter->entry == NULL) {
            dictht *ht = &iter->d->ht[iter->table];
            if (iter->index == -1 && iter->table == 0) {
                if (iter->safe)
                    iter->d->iterators++;
                else
                    iter->fingerprint = dictFingerprint(iter->d);
            }
            iter->index++;
            if (iter->index >= (long) ht->size) {
                if (dictIsRehashing(iter->d) && iter->table == 0) {
                    iter->table++;
                    iter->index = 0;
                    ht = &iter->d->ht[1];
                } else {
                    break;
                }
            }
            iter->entry = ht->table[iter->index];
        } else {
            iter->entry = iter->nextEntry;
        }
        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;
}
  1. 判断当前table索引的链表是否迭代完,如果未迭代完,继续在当前table索引迭代,如果迭代完成table索引加一。
  2. 如果字典正在rehash,说明正在使用一号表,设置节点列表为一号表ht[1]
  3. 遍历链表直接返回hash节点信息

更多讲解,欢迎关注我的github:
go成神之路

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值