字典是redis底层数据结构之一,在dict.c中实现,下面分析下他的实现。
一.简介
redis的dict仍然是一个数组+链表实现的哈希表。区别为redis的dict有两个哈希表,第二个哈希表resize时使用,如下为它头文件中定义的结构:
//entry
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
} dictEntry;
typedef struct dictht {
dictEntry **table;
unsigned long size;
unsigned long sizemask;
//当前哈希表中
unsigned long used;
} dictht;
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];
long rehashidx; //rehash进度
unsigned long iterators; /* number of iterators currently running */
} dict;
redis的字典有两个哈希表dictht[0]和dictht[1],只有rehash时两个哈希表才同时使用,平时只有dictht[0]哈希表中有数据。
rehashidx用来标记rehash正在执行到哪个槽,如果没有rehashing则返回-1.
redis rehash最大的特点是渐进性rehash,将rehash操作分散在插入,删除等操作中。这点和ThreadPoolExecutor实现思想很像有木有。
二.实现分析:
1.插入:
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;
}
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{
long index;
dictEntry *entry;
dictht *ht;
//如果正在rehashing,则执行一次_dictRehashStep操作。
if (dictIsRehashing(d)) _dictRehashStep(d);
//获取插入的key在数组的中的位置
if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)
return NULL;
//如果正在执行rehash操作则将entry插入ht[1],否在插入ht[0]
ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
entry = zmalloc(sizeof(*entry));
//放入链表头
entry->next = ht->table[index];
ht->table[index] = entry;
//负载+1
ht->used++;
dictSetKey(d, entry, key);
return entry;
}
//返回一个key所在的数组下标并判断该key是否存在。如果没有执行rehash,则只搜索ht[0]并返回该key在ht[0]的下标。如果正在rehash则两个哈希表都搜索并返回在ht[1]的下标.
static long _dictKeyIndex(dict *d, const void *key, uint64_t hash, dictEntry **existing)
{
unsigned long idx, table;
dictEntry *he;
if (existing) *existing = NULL;
//如果当前负载已经达到负载因子,则扩容。
if (_dictExpandIfNeeded(d) == DICT_ERR)
return -1;
//先搜索第一个哈希表,搜索完第一个哈希表之后,如果当前字典没有在执行rehash,则不搜索第二个哈希表。如果正在执行rehash,则在搜索第二个哈希表。
for (table = 0; table <= 1; table++) {
//取余计算key应放置的数组下标
idx = hash & d->ht[table].sizemask;
//沿着当前槽的链表向后搜索是否有该key,如果有返回-1.
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;
}
//如果没有rehashing,则不再搜索ht[1]
if (!dictIsRehashing(d)) break;
}
return idx;
}
//检查当前哈希表是否需要扩容,正在rehash的话不扩容,扩容后大小为比used*2大的最接近的2的n次幂。
static int _dictExpandIfNeeded(dict *d)
{
//正在rehash的话不扩容。
if (dictIsRehashing(d)) return DICT_OK;
//如果ht[0]是空,则初始化
if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);
//负载因为为1.但是如果正在执行bgsave操作,负载因子为dict_force_resize_ratio 5
if (d->ht[0].used >= d->ht[0].size &&
(dict_can_resize ||
d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
{
//扩容,扩容数为当前已有元素数量*2
return dictExpand(d, d->ht[0].used*2);
}
return DICT_OK;
}
//扩容
int dictExpand(dict *d, unsigned long size)
{
//如果正在rehashing或者扩容之后负载仍然大于1,返回错误
if (dictIsRehashing(d) || d->ht[0].used > size)
return DICT_ERR;
dictht n; /* the new hash table */
//确保size为2的倍数
unsigned long realsize = _dictNextPower(size);
//如果扩容之后的size仍等于ht[0]size,则返回错误。
if (realsize == d->ht[0].size) return DICT_ERR;
//开辟哈希表空间
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. */
if (d->ht[0].table == NULL) {
d->ht[0] = n;
return DICT_OK;
}
//将新建的哈希表赋值给ht[1]
d->ht[1] = n;
d->rehashidx = 0;
return DICT_OK;
}
插入操作有几个要点:
1.渐进性hash:
插入时会判断当前字典是否正在rehashing,如果正在rehashing,会执行一次rehash操作.
2.二倍扩容
扩容后大小为比used*2大的最接近的2的n次幂。这样&运算即可算出key要插入的数组下标。
3.负载因子为1
但是如果dict_can_resize标志位为true,表明正在执行rdb写入操作,此时负载因子为5.
4.头插法
新的节点插入到表头
5.如果正在rehash,则新节点直接插入到ht[1]。
rehash:
int dictRehash(dict *d, int n) {
int empty_visits = n*10; /* Max number of empty buckets to visit. */
if (!dictIsRehashing(d)) return 0;
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);
//跳过空槽
while(d->ht[0].table[d->rehashidx] == NULL) {
d->rehashidx++;
if (--empty_visits == 0) return 1;
}
de = d->ht[0].table[d->rehashidx];
//将当前槽上链表的所有节点都引动到ht[1]
while(de) {
uint64_t h;
nextde = de->next;
/* Get the index in the new hash table */
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++;
}
//检查时候完成,完成的话将整个ht[1]赋值给ht[0].
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;
}
rehash操作很简单,一次rehash操作移动一个槽。rehash完成后会释放ht[0],并将ht[1]赋值给ht[0].
网友评论