redis源码注释三:字典dict.c dict.h

0. redis源码注释仓库

1. dict简介

redis的字典是使用哈希表实现的,存储key-value,每个字典有两个哈希表,0号哈希表(ht[0])是字典主要使用的哈希表,而1号哈希表(ht[1])则只有在程序对0号哈希表进行rehash时才使用。

哈希表采用拉链式,当链比较长(或者比较短)的时候就要进行rehash,尽量使得大小和保存的结点数比例维持在1:1左右。如果不满足就要进行rehash,即哈希表的扩容和缩容。

在扩容和缩容时,不是立马就完成的,而是慢慢完成的,这样可以提高实时性,所以叫做渐进式rehash。

下面看看dict和哈希表的结构体到底长啥样。

1.1 dict结构体

typedef struct dict {
    dictType *type; //和哈希表key value相关的一些函数
    void *privdata; //上面函数的私有数据
    dictht ht[2];   //两张哈希表,用于渐进式rehash
    long rehashidx; /* -1表示没在进行rehash */
    unsigned long iterators; /* 当前运作的迭代器的数量 */
} dict;

比较关键的就是两张哈希表,一个用于表示是否正在rehash的标志rehashidx,以及一些函数。
函数都在dictType结构体中,包含:

typedef struct dictType {
    uint64_t (*hashFunction)(const void *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;

整个dict的结构用《redis设计与实现》一书中的图表示:
在这里插入图片描述
每个哈希表的结构又是怎样的呢?

1.2 哈希表结构体

typedef struct dictht {
    dictEntry **table;	//哈希表的bucket,结点指针数组,结点类型为dictEntry
    unsigned long size; //指针数组的大小,即bucket的大小
    unsigned long sizemask;//指针数组的长度掩码,用于计算索引值
    unsigned long used; //结点个数
} dictht;

每个结点的类型为dictEntry:

typedef struct dictEntry {
    void *key; //key
    union {    //value
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;//指向下一个结点的指针,由于使用的是拉链法,所以有指向下一个value的指针
} dictEntry;

整个哈希表的结构如下:
在这里插入图片描述
这里解决哈希冲突的方法是拉链法,所以dictEntry结构体中有指向下一个dictEntry的指针。
为了满足近似O(1)的查找时间复杂度,链不能太长,太长就需要rehash。

整个dict的结构体如下:
在这里插入图片描述

1.3 哈希函数/哈希洪水攻击Hash-Flooding Attack

redis-5.0.8中,默认的哈希函数为siphash,之前版本的哈希函数是MurmurHash2,但是因为MurmurHash2被发现可以稳定构造碰撞函数,所以后来就改成了能抗住哈希洪水攻击的siphash

说人话,拉链法解决哈希碰撞的方式就是相同哈希值的结点串成一个链表,如果我们能根据哈希算法的漏洞,构造出许多碰撞的key,就会使哈希表的效率急剧下降。这就是哈希洪水攻击。详见:什么是哈希洪水攻击(Hash-Flooding Attack)?
在这里插入图片描述
解决的方法就是隐藏哈希函数的细节,如何隐藏呢?使用种子,使用哈希种子的哈希算法,我们称之为带密钥哈希算法(Keyed Hash Function)。SipHash就是其中一种,reids中使用的是SipHash-1-2,在siphash.c中实现。
具体流程:SipHash 算法流程

1.4 一些宏定义和API

1.4.1 宏定义

比较重要的就是:
1、DICT_HT_INITIAL_SIZE,哈希表的默认桶的数量,为4.
2、dictIsRehashing(d),返回一个dict是否在进行rehash,true表示在rehash。

/* This is the initial size of every hash table */
#define DICT_HT_INITIAL_SIZE     4 //每个哈希表的默认大小是4,就是bucket中有4个位置table[0]/table[1]/table[2]/table[3]

/* ------------------------------- Macros ------------------------------------*/
#define dictFreeVal(d, entry) \
    if ((d)->type->valDestructor) \
        (d)->type->valDestructor((d)->privdata, (entry)->v.val)//如果定义了valDestructor,则使用valDestructor释放value

#define dictSetVal(d, entry, _val_) do { \
    if ((d)->type->valDup) \
        (entry)->v.val = (d)->type->valDup((d)->privdata, _val_); \
    else \
        (entry)->v.val = (_val_); \
} while(0)//如果定义了valDup,则使用valDup复制value

#define dictSetSignedIntegerVal(entry, _val_) \
    do { (entry)->v.s64 = _val_; } while(0)//设置有符号的value

#define dictSetUnsignedIntegerVal(entry, _val_) \
    do { (entry)->v.u64 = _val_; } while(0)//设置无符号的value

#define dictSetDoubleVal(entry, _val_) \
    do { (entry)->v.d = _val_; } while(0)//设置double的value

#define dictFreeKey(d, entry) \
    if ((d)->type->keyDestructor) \
        (d)->type->keyDestructor((d)->privdata, (entry)->key)//如果定义了keyDestructor则使用keyDestructor释放key

#define dictSetKey(d, entry, _key_) do { \
    if ((d)->type->keyDup) \
        (entry)->key = (d)->type->keyDup((d)->privdata, _key_); \
    else \
        (entry)->key = (_key_); \
} while(0)//如果定义了keyDup则使用keyDup复制key

#define dictCompareKeys(d, key1, key2) \
    (((d)->type->keyCompare) ? \
        (d)->type->keyCompare((d)->privdata, key1, key2) : \
        (key1) == (key2))//如果定义了keyCompare则使用keyCompare比较两个key

#define dictHashKey(d, key) (d)->type->hashFunction(key)	//计算key的哈希值
#define dictGetKey(he) ((he)->key)							//返回结点的key
#define dictGetVal(he) ((he)->v.val)						//返回结点的value
#define dictGetSignedIntegerVal(he) ((he)->v.s64)			//返回结点有符号的value
#define dictGetUnsignedIntegerVal(he) ((he)->v.u64)			//返回结点无符号的value
#define dictGetDoubleVal(he) ((he)->v.d)					//返回结点double型的value
#define dictSlots(d) ((d)->ht[0].size+(d)->ht[1].size)		//返回字典槽的数量
#define dictSize(d) ((d)->ht[0].used+(d)->ht[1].used)		//返回字典哈希表中使用的槽的数量
#define dictIsRehashing(d) ((d)->rehashidx != -1)			//返回是否在rehash

1.4.2 API

API很多,挑几个重要的:

/* API */
dict *dictCreate(dictType *type, void *privDataPtr); 	//创建字典
int dictExpand(dict *d, unsigned long size);			//扩大字典
int dictAdd(dict *d, void *key, void *val); 			//往字典中加一个值
int dictReplace(dict *d, void *key, void *val);			//将字典d中的key的value替换成val
int dictDelete(dict *d, const void *key);				//在字典中删除key
dictEntry *dictUnlink(dict *ht, const void *key);		//从哈希表中删除某个结点但不释放
void dictFreeUnlinkedEntry(dict *d, dictEntry *he);		//释放结点
void dictRelease(dict *d);								//清空并释放dict
dictIterator *dictGetIterator(dict *d);					//创建一个不安全的迭代器
dictIterator *dictGetSafeIterator(dict *d);				//创建一个安全的迭代器
dictEntry *dictNext(dictIterator *iter);				//迭代器当前指向的结点
void dictReleaseIterator(dictIterator *iter);			//释放迭代器
dictEntry *dictGetRandomKey(dict *d);					//随机返回一个结点
unsigned int dictGetSomeKeys(dict *d, dictEntry **des, unsigned int count);//获取count个key,结点存在dictEntry中
uint64_t dictGenHashFunction(const void *key, int len);//根据key生成哈希值
uint64_t dictGenCaseHashFunction(const unsigned char *buf, int len);//同上
void dictEmpty(dict *d, void(callback)(void*));	//destroy整个dict
void dictEnableResize(void);	//允许resize
void dictDisableResize(void);	//禁止resize
int dictRehash(dict *d, int n);	//rehash
int dictRehashMilliseconds(dict *d, int ms);//rehash ms毫秒,超时则退出

比较经典的当属扩容缩容dictExpand和rehash dictRehash了,下面详细介绍。

2. 重点API介绍

2.1 创建dict相关

下面三个函数就是创建dict相关的,没啥说的。

static void _dictReset(dictht *ht)
{
    ht->table = NULL; //reset哈希表,只能被ht_destroy()调用.
    ht->size = 0;
    ht->sizemask = 0;
    ht->used = 0;
}

/* 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;			//没在rehash
    d->iterators = 0;
    return DICT_OK;
}

2.2 调整大小dictResize、dictExpand

dictResize的作用是:调整哈希表的大小,使其能包含所有元素,但是尽可能地小,基本可以理解为,有多少萝卜挖多少坑。当然哈希表最小尺寸为4.

int dictResize(dict *d)
{
    int minimal;

    if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;//如果dict不能resize或者在rehash则返回错误
    minimal = d->ht[0].used;//已有的节点数
    if (minimal < DICT_HT_INITIAL_SIZE)
        minimal = DICT_HT_INITIAL_SIZE;//如果节点数小于哈希表初始大小4,则设置为初始大小
    return dictExpand(d, minimal);//然后将哈希表大小扩展到minimal
}

重点是dictExpand。

/* Expand or create the hash table */
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) //如果size小于哈希表中结点的数量则返回错误,因为缩到size将损失部分值
        return DICT_ERR;

    dictht n; /* 新的哈希表 */
    unsigned long realsize = _dictNextPower(size);//获取大于size的最小的2的N次幂

    /* Rehashing to the same table size is not useful. */
    if (realsize == d->ht[0].size) return DICT_ERR;//如果realsize大小等于d->ht[0].size返回错误,不需要expand

    /* Allocate the new hash table and initialize all pointers to NULL */
    n.size = realsize;
    n.sizemask = realsize-1;
    n.table = zcalloc(realsize*sizeof(dictEntry*));//table的大小为realsize
    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) { //如果ht[0]为空则将ht[0]设置为新的哈希表,不用渐进式rehash
        d->ht[0] = n;
        return DICT_OK;
    }

    /* Prepare a second hash table for incremental rehashing */
    d->ht[1] = n;	 //ht[[0]不为空则将ht[1]设置为新的hash表
    d->rehashidx = 0;//进行渐进式rehash
    return DICT_OK;
}

2.3 rehash/dictRehashMilliseconds

作为面试时经常被问到的一个点,刚开始时确实一头雾水,在看了《redis设计与实现》之后逐渐了解了rehash的过程。

  1. 为什么要进行rehash?
    关键是链地址法在处理碰撞的时候,如果链表太长的话就会引起哈希表退化。

  2. 什么时候会引发rehash?
    1)当used/size>=1时;
    2)进行持久化时可能会导致used/size>=5,此时要强制rehash。
    3)收缩时也会造成rehash。

  3. rehash的过程
    为ht[1]分配空间,大小为ht[0]的两倍;
    然后将ht[0]逐渐迁移到ht[1](渐进式rehash,不是一下子完成的)
    然后释放ht[0]的空间,并将ht[1]代替ht[0];
    创建一个新的ht,将其设置为ht[1];
    将字典的rehashidx 属性设置为-1 ,标识rehash 已停止;

  4. 渐进式rehash
    1)在一个有很多键值对的字典里,某个用户在添加新键值对时触发了rehash过程,如果这个rehash 过程必须将所有键值对迁移完毕之后才将结果返回给用户,这样的处理方式将是非常不友好的。
    2)另一方面,要求服务器必须阻塞直到rehash 完成,这对于Redis 服务器本身也是不能接受的

接下来我们看一下rehash的代码:

int dictRehash(dict *d, int n) {
    int empty_visits = n*10; /* Max number of empty buckets to visit. */
    if (!dictIsRehashing(d)) return 0;//如果没在rehash直接返回

    while(n-- && d->ht[0].used != 0) { //执行n次渐进式rehash
        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);//要搞明白rehashidx的含义:ht[0]的table中下标为rehashidx的为本次搬移的对象
        while(d->ht[0].table[d->rehashidx] == NULL) { //table[rehashidx]位置的桶为空则继续,知道找到非空桶或者空桶数量达到empty_visits
            d->rehashidx++;
            if (--empty_visits == 0) return 1;
        }
        de = d->ht[0].table[d->rehashidx];//de就是本次要搬移的链表头结点
        /* Move all the keys in this bucket from the old to the new hash HT */
        while(de) {
            uint64_t h;

            nextde = de->next;//先保存一下de->next
            /* Get the index in the new hash table */
            h = dictHashKey(d, de->key) & d->ht[1].sizemask;//获得de在ht[1]的table中的下标,就是搞清楚在哪个桶
            de->next = d->ht[1].table[h];//de插入到table[h]中
            d->ht[1].table[h] = de;
            d->ht[0].used--;//每移动一个结点,ht[0]中的结点就少一个
            d->ht[1].used++;//相反,ht[1]中的结点就多一个
            de = nextde;//继续移动这条链表的结点
        }
        d->ht[0].table[d->rehashidx] = NULL;//移动完毕后将ht[0]table中的这个桶设置为空
        d->rehashidx++;//继续移动ht[0]table中的下一个桶
    }

    /* Check if we already rehashed the whole table... */ //之所以进行下面的判断是因为rehash是渐进式的,n次rehash不一定可以搬运完成
    if (d->ht[0].used == 0) { 	//如果ht[0]的所有结点都转移到ht[1]了,说明rehash完成,此时将ht[0]释放,并将ht[1]替代ht[0]
        zfree(d->ht[0].table); 	//释放ht[0]
        d->ht[0] = d->ht[1];	//ht[1]替代ht[0]
        _dictReset(&d->ht[1]);	
        d->rehashidx = -1;		//rehash完成
        return 0;
    }

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

从程序看出,dictRehash函数每次执行n次rehash,即使rehash没有完成也要返回。

除了rehash,还有几个其他几个和rehash相关的函数,不过都是在内部调用rehash。

dictRehashMilliseconds表示rehash一定的时间,时间到之后,即使没有rehash完成也要返回。

long long timeInMilliseconds(void) { //获得当前的毫秒数
    struct timeval tv;

    gettimeofday(&tv,NULL);
    return (((long long)tv.tv_sec)*1000)+(tv.tv_usec/1000);
}

/* Rehash for an amount of time between ms milliseconds and ms+1 milliseconds */
int dictRehashMilliseconds(dict *d, int ms) { //rehash ms毫秒,大于该时间就停止,或者在这之前rehash完成也停止
    long long start = timeInMilliseconds(); //获得骑士时间
    int rehashes = 0;//这个过程中rehash的次数

    while(dictRehash(d,100)) { //每次rehash 100次
        rehashes += 100;
        if (timeInMilliseconds()-start > ms) break;//如果执行用时超过ms则直接退出
    }
    return rehashes;//返回rehash的次数
}

2.4 添加键值

2.4.1 添加一个结点dictAddRaw

dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)

该函数添加一个结点,结点的键为key,返回指向结点的指针,调用者可以对返回的指针进行操作(如设置结点的值),如果key已经存在则返回空。

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)//查看key所在的槽,如果返回-1说明key已经存在
        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];//如果正在执行rehash则在ht[1]上添加一个结点,否则在ht[0]上添加结点
    entry = zmalloc(sizeof(*entry));//分配一个结点
    entry->next = ht->table[index]; //将结点放入对应bucket的头部
    ht->table[index] = entry;		
    ht->used++;//结点数量增加

    /* Set the hash entry fields. */
    dictSetKey(d, entry, key);//设置结点的key
    return entry;
}

2.4.2 添加结点并设置值dictAdd

int dictAdd(dict *d, void *key, void *val)

添加一个结点,其键为key,值为val。
如果键已经存在则返回DICT_ERR,并且不会改变原来键对应的值。

/* Add an element to the target hash table */
int dictAdd(dict *d, void *key, void *val)
{
    dictEntry *entry = dictAddRaw(d,key,NULL);//在字典中新加入一个键为key的结点

    if (!entry) return DICT_ERR;	//如果entry为空说明key已经存在,此时不改变原来的值直接返回(替换由replace函数完成)
    dictSetVal(d, entry, val);		//设置key所对应的val
    return DICT_OK;
}

2.4.3 替换键值dictReplace

int dictReplace(dict *d, void *key, void *val)

该函数将key的值替换为val:
如果key不存在,则直接新增一个结点然后将其值设置为val即可;
如果key存在,则需要将原来的值释放并将键对应的值设置为新的val。

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);//尝试添加一个结点,如果key存在则entry为空
    if (entry) {
        dictSetVal(d, entry, val);//entry不为空说明key不存在,设置结点的val
        return 1;
    }

    /* Set the new value and free the old one. Note that it is important
     * to do that in this order, as the value may just be exactly the same
     * as the previous one. In this context, think to reference counting,
     * you want to increment (set), and then decrement (free), and not the
     * reverse. */
    auxentry = *existing;			//之前的val备份,以便释放
    dictSetVal(d, existing, val);	//设置新的val
    dictFreeVal(d, &auxentry);		//释放旧的val
    return 0;
}

2.5 删除键值

2.5.1 查找并删除某个结点dictGenericDelete

static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree)

该函数查找键为key的结点并将其删除,如果找到则返回指向结点的指针,如果找不到则返回NULL。
如果nofree为0则释放掉结点,否则不释放。
该函数一般被dictDeletedictDelete调用。

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;//两个表都为空返回NULL

    if (dictIsRehashing(d)) _dictRehashStep(d);
    h = dictHashKey(d, key);//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)) { //将当前结点的key和要查找的key进行比较
                /* Unlink the element from the list */
                if (prevHe)
                    prevHe->next = he->next; //将该节点从链表中删除,非头结点
                else
                    d->ht[table].table[idx] = he->next;//头结点应该这样删除
                if (!nofree) {			//如果nofree为0则释放掉he,像dictDelete就会释放
                    dictFreeKey(d, he); //否则不释放,dictUnlink不会释放
                    dictFreeVal(d, he);
                    zfree(he);
                }
                d->ht[table].used--;
                return he;
            }
            prevHe = he;//当前键不是要查找的键,继续在链表上查找(统一链表哈希值相同,key value不同)
            he = he->next;
        }
        if (!dictIsRehashing(d)) break;
    }
    return NULL; /* not found *///找不到返回NULL
}
if (!nofree) {			//如果nofree为0则释放掉he,像dictDelete就会释放
   dictFreeKey(d, he); //否则不释放,dictUnlink不会释放
    dictFreeVal(d, he);
    zfree(he);
}
d->ht[table].used--;
return he;//即使已经被free了也会返回

虽然he可能被释放掉了,但是依然将其返回,至于怎么用he是调用dictGenericDelete函数者的责任。
free掉一个指针,不将其置空,指针并不会指向空,还可以使用但是很危险。

2.5.2 删除并释放结点dictDelete

int dictDelete(dict *ht, const void *key) {
    return dictGenericDelete(ht,key,0) ? DICT_OK : DICT_ERR;//删除并释放key对应的结点
}

该函数直接调用dictGenericDelete并将nofree设置为0,会释放结点,返回非空表示找到了结点并将其释放,返回空表示没找到结点。

2.5.3 删除但不释放结点dictUnlink

dictEntry *dictUnlink(dict *ht, const void *key) {
    return dictGenericDelete(ht,key,1); //从table中删除这个结点,但是不释放,将其返回,以后备用
}

这个函数也是直接调用dictGenericDelete,不过和dictDelete不同的是,该函数不会将原来的结点释放掉,也就是返回的结点仍然可以使用,用户在使用完之后需要调用dictFreeUnlinkedEntry将结点释放掉。

//释放结点,与dictUnlink配套使用
void dictFreeUnlinkedEntry(dict *d, dictEntry *he) { 
    if (he == NULL) return;
    dictFreeKey(d, he);
    dictFreeVal(d, he);
    zfree(he);
}

如:

entry = dictUnlink(dictionary,entry);
// Do something with entry
dictFreeUnlinkedEntry(entry); // <- This does not need to lookup again.

2.6 释放哈希表和字典

2.6.1 释放哈希表_dictClear

释放哈希表的过程无非是对每个桶进行遍历,删除并释放链表中的每个结点。

/* Destroy an entire dictionary */
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 */
}

2.6.2 释放字典dictRelease

释放字典就是在释放2张哈希表。

void dictRelease(dict *d) //释放整个字典
{
    _dictClear(d,&d->ht[0],NULL);//释放哈希表0
    _dictClear(d,&d->ht[1],NULL);//释放哈希表1
    zfree(d);
}

2.7 查找

查找一个key对应的值分两步:
1)查找key对应的结点;
2)如果结点存在在获取值,结点不存在返回NULL。

2.7.1 查找key对应的结点dictFind

dictEntry *dictFind(dict *d, const void *key) //查找key对应的结点,不存在返回空
{
    dictEntry *he;
    uint64_t h, idx, table;

    if (d->ht[0].used + d->ht[1].used == 0) return NULL; /* 字典为空直接返回 */
    if (dictIsRehashing(d)) _dictRehashStep(d);
    h = dictHashKey(d, key); //获得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;//当前结点的Key与要找的key相同,返回当前结点
            he = he->next;//否则继续遍历
        }
        if (!dictIsRehashing(d)) return NULL;
    }
    return NULL;//找不到返回空
}

2.7.2 查找key对应的val——dictFetchValue

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

    he = dictFind(d,key);//先找到key对应的结点
    return he ? dictGetVal(he) : NULL;//如果能找到结点则获取值,不能找到返回空
}

2.8 迭代器相关

2.8.1 字典的指纹fingerprint

/*
 * 字典的指纹是将字典的一些信息异或得到的,
 * 当一个不安全的迭代器初始化时我们获取一次字典的指纹,
 * 当期释放时再获取一次字典的指纹,
 * 如果两者不同则说明迭代器的使用着在迭代过程中执行了禁止的操作
 */
long long dictFingerprint(dict *d) { 
    long long integers[6], hash = 0;
    int j;

    integers[0] = (long) d->ht[0].table;//参与异或的是哈希表的地址,大小,已经结点个数
    integers[1] = d->ht[0].size;
    integers[2] = d->ht[0].used;
    integers[3] = (long) d->ht[1].table;
    integers[4] = d->ht[1].size;
    integers[5] = d->ht[1].used;

    /* We hash N integers by summing every successive integer with the integer
     * hashing of the previous sum. Basically:
     *
     * Result = hash(hash(hash(int1)+int2)+int3) ...
     *
     * This way the same set of integers in a different order will (likely) hash
     * to a different number. */
    for (j = 0; j < 6; j++) {
        hash += integers[j];
        /* For the hashing step we use Tomas Wang's 64 bit integer hash. */
        hash = (~hash) + (hash << 21); // hash = (hash << 21) - hash - 1;
        hash = hash ^ (hash >> 24);
        hash = (hash + (hash << 3)) + (hash << 8); // hash * 265
        hash = hash ^ (hash >> 14);
        hash = (hash + (hash << 2)) + (hash << 4); // hash * 21
        hash = hash ^ (hash >> 28);
        hash = hash + (hash << 31);
    }
    return hash;
}

2.8.2 获取一个迭代器

迭代器有安全不安全两种。

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

dictIterator *dictGetSafeIterator(dict *d) { //获取一个安全的迭代器
    dictIterator *i = dictGetIterator(d);//先获取一个不安全的迭代器

    i->safe = 1;//然后将其safe属性设置为1
    return i;
}

2.8.3 获取下一个结点dictNext

我们先想一下在字典(两个哈希表)中遍历的场景,想想自己是迭代器。

  1. 如果迭代器指向的结点为空
    1)可能是第一次迭代,初始化是将iter->entry = NULL;
    2)可能是到达了一个桶中链表的尾部,需要转移到下一个桶
    3)到达了整个哈希表的尾部,可能需要转移到下一个哈希表或者返回NULL
  2. 迭代器指向不为空,获取下一个结点即可

代码的流程基本如上,几个if-else需要仔细考虑到当时是什么情况:

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) { //如果正在rehash且是在遍历ht[0],再遍历就遍历到ht[1]上了
                    iter->table++;	//到ht[1]上遍历
                    iter->index = 0;//桶号变为0
                    ht = &iter->d->ht[1];
                } else {
                    break;//不是在rehash,那么遍历到ht[0]的末尾了,往后没元素了,退出返回NULL
                }
            }
            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;
}

3. 总结

1)首先要仔细研究和字典有关的几个结构体,这样便于了解哈希表具体实现、rehash的过程、迭代器迭代的过程;
2)了解哈希函数的选择,为什么选择SipHash,什么是哈希洪水攻击;
3)了解哈希表扩容以及缩容的过程;
4)了解rehash的过程,什么时候会rehash;
5)一些API,仔细研究发现实现并不难,但是我不会哈哈哈。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值