字典是用来保存键值对的抽象数据结构,在Redis中的应用非常广泛,比如Redis的数据库就是使用字典来作为底层实现的,对数据库的增删改查操作也是在对字典的操作之上的。首先需要了解的是dict的基础,分别有字典dict,哈希表dictht,哈希表节点dictEntry这三个组成部分。他们之间的关系见下图
接下来需要看下这些节点在代码中是如何定义的
//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;
下面是一些主要函数的介绍,可以结合下面问题详述结合一起看
/* 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;
}
dict和hashmap的实现原理一样,都是通过数组和链表组合来实现,首先利用数组来表示第一次加入的dictEntry,这时数组也可以称为一个一个的桶。我们来看dictht结构体的size参数,就是表示了这个数组的大小,也就是表示有几个桶。如果在添加dictEntry时发生冲突,这时候我们就在这个桶下面建立链表来解决冲突。
但是随着我们不断加入dictEntry,总是需要在适当的时候进行扩容。但是什么时候进行扩容,怎么扩容,在扩容时再进行添加dictEntry该怎么办,这些问题都需要考虑。
首先在代码中,添加dictEntry节点的方法是dictAdd,下面是dictadd函数的主要调用过程
_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;
}