redis源码分析四--dict字典实现

自定义标题

      • 1、dict结构介绍
      • 2、字典实现的相关细节
        • 2.1、主要函数详解
        • 2.1、dict细节实现
          • 何时进行扩容?
          • 怎么扩容?
          • 扩容多大?
          • 安全迭代器的作用?

1、dict结构介绍

  字典是用来保存键值对的抽象数据结构,在Redis中的应用非常广泛,比如Redis的数据库就是使用字典来作为底层实现的,对数据库的增删改查操作也是在对字典的操作之上的。首先需要了解的是dict的基础,分别有字典dict,哈希表dictht,哈希表节点dictEntry这三个组成部分。他们之间的关系见下图
redis源码分析四--dict字典实现_第1张图片

  接下来需要看下这些节点在代码中是如何定义的

//dictEntry是字典结构中最小的单元,表示一个节点,其中包含了key,value和指向下一个节点单元的指针。
typedef struct dictEntry {
    void *key;   
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;  //利用union共享同一块内存,可以保证空间的合理利用
    struct dictEntry *next;
} dictEntry;
//dictht表示一个字典表,直接对dictEntry节点进行管理
typedef struct dictht {
    dictEntry **table;
    unsigned long size;   //表的长度,可以容纳几个桶,请注意这和dictEntry数量不同,因为一个桶上可以存在链表,即一个桶上对应多个dictEntry
    unsigned long sizemask;  //利用这个参数我们可以将对应的hash值和这个sizemask进行&,从而得到hash和size求与后的值
    unsigned long used;   //当前在表中存在几个dictEntry
} dictht;
//dict是对dictht的一层封装,我们可以利用dict来进行rehash
typedef struct dict {
    dictType *type;   //当前进行操作的类型
    void *privdata;
    dictht ht[2];     //表示两张dictht表
    long rehashidx; // 表示当前rehash的位置,如果是-1表示没有今星期rehash
    unsigned long iterators; //当前指向当前dict的dictIterator的数量
} dict;
//dictIterator可以用来遍历任意一个dict上dictEntry节点
typedef struct dictIterator {
    dict *d;
    long index;
    int table, safe;
    dictEntry *entry, *nextEntry;
    /* unsafe iterator fingerprint for misuse detection. */
    long long fingerprint;
} dictIterator;
typedef struct dictType {
    uint64_t (*hashFunction)(const void *key);  //hash函数
    void *(*keyDup)(void *privdata, const void *key); //对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;

2、字典实现的相关细节

2.1、主要函数详解

下面是一些主要函数的介绍,可以结合下面问题详述结合一起看

/* Add an element to the target hash table */
int dictAdd(dict *d, void *key, void *val)
{
	//主要调用dictdictAddRaw来实现
    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);

    //获取当前key在dict中对应的下标,在这个函数中调用_dictExpandIfNeeded检查是否需要进行扩容
    if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)
        return NULL;

    //如果是rehash状态,则添加在ht[1]中,因为这时候ht[0]我们已经逐渐地将ht[0]中的数据转移到ht[1]中
    ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
    entry = zmalloc(sizeof(*entry));
    //用了头插法加入entry
    entry->next = ht->table[index];
    ht->table[index] = entry;
    ht->used++;

    dictSetKey(d, entry, key);
    return entry;
}
/* Expand the hash table if needed */
static int _dictExpandIfNeeded(dict *d)
{
    //如果正在进行rehash,则不能进行扩容
    if (dictIsRehashing(d)) return DICT_OK;

    //这时候dict的哈希表还没有数据,需要初始化ht[0]的size
    if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);

    
    //在我们允许进行扩容时并且dictht表中的entry的个数:桶数>5时,需要将进行扩容,扩容的参数used*2并不代表最后的桶数,因为还需要满足其为大于等于used*2的最小的2的倍数
    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)
{
    //判断当前的size是否大于used,因为我们是按照size为used的2倍传入的,这里做一下判断数据大小的情况
    if (dictIsRehashing(d) || d->ht[0].used > size)
        return DICT_ERR;

    dictht n; /* the new hash table */
    //这个realsize就是真正需要申请的桶的数量,这个_dictNextPower就是求得最小比size大的2的倍数,例如传入的如果是3,则realsize为4,传入的是7,则为8,传入的为8,则也为8
    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;

    //这种情况就是我们初始化的时候,第一次ht[0]还没有初始化,也是利用这个函数申请空间
    if (d->ht[0].table == NULL) {
        d->ht[0] = n;
        return DICT_OK;
    }

    //这种情况就是原先ht[0]表不够大,需要进行扩容
    d->ht[1] = n;
    d->rehashidx = 0;
    return DICT_OK;
}
int dictRehash(dict *d, int n) {
    /*一次操作时可以跳过空桶的数量,因为我们在访问一个dictht上时,对应的桶不一定有值,例如在d->ht[1]上没有值,则视作一个空桶,如果在连续查询empty_visits都遇到空桶,则这次操作结束*/
    int empty_visits = n*10; 
    //再次判断当前rehashid是否为0,0这种情况是无法处理的,直接返回错误
    if (!dictIsRehashing(d)) return 0; 
    //n表示我们可以将dictht上几个桶从旧的hashtable中移到新的hashtable,used表示旧的表中还剩余几个dictEntry,当dictEntry为0时表示旧表上的节点都已经移到新表上了
    while(n-- && d->ht[0].used != 0) { 
        dictEntry *de, *nextde;
        assert(d->ht[0].size > (unsigned long)d->rehashidx); //确保当前rehashidx可以在ht上找到指定的桶
        while(d->ht[0].table[d->rehashidx] == NULL) {
            //自增一,当下次进行操作时就是下一个桶
            d->rehashidx++;
            //连续访问到empty_visits个空桶后结束此次操作
            if (--empty_visits == 0) return 1;  
        }
         //找到需要移动的桶的位置
        de = d->ht[0].table[d->rehashidx];  
       
        while(de) {
            uint64_t h;

            nextde = de->next; 
            //dictHashKey(d, de->key)函数找到对应的hash值,和d->ht[1].sizemask进行操作可以得到hash和d->ht[1].size的余数,从而得到当前在新表中桶的位置
            h = dictHashKey(d, de->key) & d->ht[1].sizemask;  
            //下面这两步操作也就是链表的头插入法
            de->next = d->ht[1].table[h];
            d->ht[1].table[h] = de;
            //旧的表中dictEntry的数量减一,新的表中dictEntry的数量加一   
            d->ht[0].used--;
            d->ht[1].used++;
            //如果该桶的链表还没有结束,则继续进行下去
            de = nextde;
        }
        //旧的表对应的桶进行销毁
        d->ht[0].table[d->rehashidx] = NULL;
        d->rehashidx++;
    }

   //保证旧表上已经没有了dictEntry
    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;
}

2.1、dict细节实现

  dict和hashmap的实现原理一样,都是通过数组和链表组合来实现,首先利用数组来表示第一次加入的dictEntry,这时数组也可以称为一个一个的桶。我们来看dictht结构体的size参数,就是表示了这个数组的大小,也就是表示有几个桶。如果在添加dictEntry时发生冲突,这时候我们就在这个桶下面建立链表来解决冲突。

  但是随着我们不断加入dictEntry,总是需要在适当的时候进行扩容。但是什么时候进行扩容,怎么扩容,在扩容时再进行添加dictEntry该怎么办,这些问题都需要考虑。

  首先在代码中,添加dictEntry节点的方法是dictAdd,下面是dictadd函数的主要调用过程
redis源码分析四--dict字典实现_第2张图片

何时进行扩容?

  _dictKeyIndex函数在获取一个entry对应哪一个桶下标时会去判断当前的哈希表是否需要扩容,在第一次创建时需要进行扩容,也就是ht[0]的size为0时需要进行扩容,还有一种是当ht[0]中used:size>5时也需要进行扩容,因为这时候认为冲突已经很严重了。

怎么扩容?

  初始化ht[0]这种情况就不需要说明,就是简单的申请size为4的桶空间。在我们因为ht[0]不够大而需要进行扩容时具体是怎么做的呢?因为扩容不一定原来的地址下还能顺序容纳增大后的空间,所以我们往往需要重新申请一块地址来存放数据,然后将原来表中的数据拷贝到新的表中,但是redis是单线程程序,如果一次性全部拷贝,这样其他程序就不能进行操作了,对用户体验很差,所以redis中的做法是逐步拷贝。申请一个ht[1]表,然后我们在查数据,添加数据,删除数据这类的简单操作时附加一次rehash操作,效果是一次拷贝一个桶的旧表中的数据到新表。

扩容多大?

   首先我们是在used:size>5的情况下进行扩容,这时候我们将需要申请的size大小初步填为size2,但是我们为了满足利用&来计算余数的,例如如果是2的幂,例如8,我们可以有一个mask为8-1,他的二进制则为0111,这样3&7就是3,我们可以用二进制来求得余数,这对于不满住二进制的数据时不满足的,所以我们还需要将size2扩大为最小满足为2的幂的数据。这个数就是扩容的大小。

安全迭代器的作用?

   如果有安全迭代器存在,则不允许进行rehash,反应到程序中是在_dictRehashStep函数中判断d->iterators是否为0,如果不为0,则不会调用dictRehash来进行rehash。而安全迭代器是什么时候加上去的呢?是在我们调用dictNext使用迭代器进行查找下一个dictEntry的时候进行。

dictEntry *dictNext(dictIterator *iter)
{
    while (1) {
    	
        if (iter->entry == NULL) {
            dictht *ht = &iter->d->ht[iter->table];
            //判断该iter的对象是否是第一次调用,这时候的参数还是初始化状态
            if (iter->index == -1 && iter->table == 0) {
            	//判断这个迭代器是否是安全迭代器
                if (iter->safe)
                	//如果是安全迭代器,则将表示迭代器数量的iterators加一,在dictReleaseIterator中可以将这个数值减一
                    iter->d->iterators++;
                else
                	//如果是不安全的迭代器,则需要比较前后两次的指纹,所以这时候需要记录一下指纹信息,指纹信息是根据两个ht的位置,used,size等信息处理得到的
                    iter->fingerprint = dictFingerprint(iter->d);
            }
            //更新索引
            iter->index++;
            //如果在ht[0]中已经达到了最后的位置,需要判断是否在进行rehash,如果在进行rehash,则还需要移动到一下ht[1]
            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;
                }
            }
            //在上面整理出最后的索引下标中在这里根据索引指向entry
            iter->entry = ht->table[iter->index];
        } else {
            iter->entry = iter->nextEntry;
        }
        //因为在迭代过程中数据在变化,所以还需要保存下一个entry的信息,这里考虑的不是很全面,没有弄清楚,等之后再深入理解一下
        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;
}
  • 如果 safe 属性的值为 1 ,也就是安全迭代器,那么在迭代进行的过程中,程序仍然可以执行 dictAdd 、 dictFind 和其他函数,对字典进行修改。
  • 如果 safe 不为 1 ,也就是非安全迭代器,那么程序只会调用 dictNext 对字典进行迭代,而不对字典进行修改。

你可能感兴趣的:(redis)