【Redis 源码】3dict字典数据结构
1 数据结构说明
dictionary数据结构,也称为哈希表(hash table)。用于存储字典。哈希是一个键值对的集合,每个键都是唯一的并与一个值相关联。字典的设计旨在提供高效的查找、插入和删除操作。
2 核心数据结构
hash 的数据结构定义在头文件 dict.h
中,核心数据结构为:
typedef struct dict {
dictType *type; /* 字典类型 */
void *privdata; /* 私有数据 */
dictht ht[2]; /* 两个散列表 */
long rehashidx; /* 重新散列索引,如果不是正在进行重新散列,则为 -1 */
int16_t pauserehash; /* 如果大于 0,则暂停重新散列,小于 0 表示编码错误.比如在dictScan时候就给 pauserehash值++,表示正在扫描。停止刷新*/
} dict;
/* 散列表结构体 */
typedef struct dictht {
dictEntry **table; /* 散列表数组 */
unsigned long size; /* 数组长度 */
unsigned long sizemask; /* 数组长度减一后的值,用于计算索引 */
unsigned long used; /* 已使用的槽数 */
} dictht;
/* 字典条目结构体 */
typedef struct dictEntry {
void *key; // 键的指针
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v; /* 值 */
struct dictEntry *next; // 指向下一条目的指针
} dictEntry;
- dictt的ht属性是一个包含两个项的数组,数组中的每个项都是一个dictht哈希表,一般情况下,字典只使用ht[0]哈希表,ht[1]哈希表只会在对ht[0]哈希表进行rehash时使用。
- dictht 哈希表,
table
属性是一个数组,数组中的每个元素都是一个指向dictEntry
结构的指针,每个dictEntry
结构保存着一个键值对。 - dictEntry 字典节点,dict解决冲突的办法是采用链表的链式解决方案,所以发生冲突的节点通过
next
指针链接在一起。哈希节点的键是void*
类型的,可以表示任意类型的键。
其他结构说明
- dictType
typedef struct dictType {
//计算hash方法
uint64_t (*hashFunction)(const void *key);
//复制key方法
void *(*keyDup)(void *privdata, const void *key);
//复制value方法
void *(*valDup)(void *privdata, const void *obj);
// 判断两个key相等的方法
int (*keyCompare)(void *privdata, const void *key1, const void *key2);
//销毁key方法
void (*keyDestructor)(void *privdata, void *key);
//销毁value方法
void (*valDestructor)(void *privdata, void *obj);
} dictType;
3 核心代码
dictAdd 添加
int dictAdd(dict *d, void *key, void *val)
{
// 添加key,并创建一个新的dictEntry节点
dictEntry *entry = dictAddRaw(d,key,NULL);
//如果返回的entry为空说明添加失败
if (!entry) return DICT_ERR
//为当前节点设置我们需要的值
dictSetVal(d, entry, val);
//返回设置成功
return DICT_OK;
}
在上面代码中参数有3个:dict数据结构、我们需要保存的key和value。
其中主要分为两个步骤:
- 根据我们输入的key在dict结构中插入节点,并将新创建的节点dictEntry返回。
- 将我们需要保存的value设置到新创建的dictEntry节点中。
dictSetVal 是一个宏定义,根据dictEntry.type的valDup函数来确定,当前值是否复制一个新的值还是直接进行引用
#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)
- dictAddRaw 根据key在dict中添加新的dictEntry节点并返回,如果key存在,则返回-1.并将历史dictEntry赋值给existing
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{
long index;
dictEntry *entry;
dictht *ht;
//当前字典是否正在扩容,当标志位dict.rehashidx不为-1时,说明在扩容
//当节点正在扩容,则调用dictRehash 进行刷新
if (dictIsRehashing(d)) _dictRehashStep(d);
/* 查找当前key,在table 的索引下标。并且检查当前key是否已经在dict中存在,如果存在则返回-1*/
if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)
return NULL;
/*根据是否正在刷新,如果在刷新,则返回 ht1 ,否则ht0 */
ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
//分配新的entry空间,并使用头插法,将插入链表中
entry = zmalloc(sizeof(*entry));
entry->next = ht->table[index];
ht->table[index] = entry;
ht->used++;
/* 设置字典表的key逻辑类似 dictSetVal */
dictSetKey(d, entry, key);
return entry;
}
- _dictKeyIndex
/**
dict *d:指向 dict 结构体的指针,即字典实例。
const void *key:指向键的指针。
uint64_t hash:键的散列值。
dictEntry **existing:如果键已经存在于字典中,则通过这个指针返回对应的 dictEntry。
*/
static long _dictKeyIndex(dict *d, const void *key, uint64_t hash, dictEntry **existing)
{
unsigned long idx, table;
dictEntry *he;
if (existing) *existing = NULL;
/* 如果需要扩展散列表,调用 _dictExpandIfNeeded 函数进行扩展。如果扩展失败,则返回 -1。 */
if (_dictExpandIfNeeded(d) == DICT_ERR)
return -1;
/*循环遍历两个散列表(如果正在进行重新散列),直到找到合适的索引位置或确认键不存在。*/
for (table = 0; table <= 1; table++) {
/*使用散列值和 sizemask 计算键在散列表中的索引位置。*/
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;
}
/*如果没有在刷新,就只需要查找ht0即可*/
if (!dictIsRehashing(d)) break;
}
return idx;
}
- _dictExpandIfNeeded 用于判断是否需要扩展散列表
static int _dictExpandIfNeeded(dict *d)
{
/* 如果字典正在进行重新散列(dictIsRehashing 返回真 */
if (dictIsRehashing(d)) return DICT_OK;
/* 如果散列表为空(即大小为零),则将其扩展到初始大小 DICT_HT_INITIAL_SIZE。 */
if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);
/* 1、如果散列表的使用率达到了 100% used >= size
2、允许自动扩展(dict_can_resize 为真)
3、即使不允许自动扩展,但负载因子超过了设定的安全阈值
*/
if (d->ht[0].used >= d->ht[0].size &&
(dict_can_resize ||
d->ht[0].used/d->ht[0].size > dict_force_resize_ratio) &&
dictTypeExpandAllowed(d))
{
return dictExpand(d, d->ht[0].used + 1);
}
return DICT_OK;
}
dictExpand 扩容
/*
dict *d:指向 dict 结构体的指针,即字典实例。
unsigned long size:目标散列表的大小。
int* malloc_failed:一个指向整型变量的指针,用于指示内存分配是否失败。
*/
int _dictExpand(dict *d, unsigned long size, int* malloc_failed)
{
if (malloc_failed) *malloc_failed = 0;
/* 检查目标大小是否有效。如果正在进行重新散列,或者目标大小小于当前散列表中已使用的条目数,则返回 DICT_ERR */
if (dictIsRehashing(d) || d->ht[0].used > size)
return DICT_ERR;
dictht n; /* the new hash table */
//根据size,计算大于或等于给定大小的最小的 2 的幂次方
unsigned long realsize = _dictNextPower(size);
/* 如果实际目标大小与当前散列表大小相同,则不需要扩展,返回 DICT_ERR。 */
if (realsize == d->ht[0].size) return DICT_ERR;
/* 创建新的数组,并对dictht进行初始化设置 */
n.size = realsize;
n.sizemask = realsize-1;
if (malloc_failed) {
n.table = ztrycalloc(realsize*sizeof(dictEntry*));
*malloc_failed = n.table == NULL;
if (*malloc_failed)
return DICT_ERR;
} else
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;
}
/* 将新创建的 dictht 赋值给ht1*/
d->ht[1] = n;
d->rehashidx = 0;
return DICT_OK;
}
dictReplace 跟新数据
dictReplace
函数的作用是在 Redis 的字典中替换一个键对应的值。如果键已经存在,则更新其值;如果键不存在,则新增一个键值对。这个函数结合了添加新条目和更新现有条目的功能,使得字典能够在键存在时替换值,而在键不存在时添加新条目。
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);
if (entry) {
dictSetVal(d, entry, 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;
dictSetVal(d, existing, val);
dictFreeVal(d, &auxentry);
return 0;
}
dictGenericDelete 删除数据
/*
dict *d:指向 dict 结构体的指针,即字典实例。
const void *key:指向要删除的键的指针。
int nofree:如果为非零值,则不释放键和值的内存。
返回值:dictEntry *:指向被删除的 dictEntry,如果未找到对应的键,则返回 NULL。
*/
static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) {
uint64_t h, idx;
dictEntry *he, *prevHe;
int table;
//如果两个散列表都没有使用任何条目,则直接返回 NULL
if (d->ht[0].used == 0 && d->ht[1].used == 0) return NULL;
//如果正在进行重新散列,则调用 _dictRehashStep 函数进行一次重新散列步骤。
if (dictIsRehashing(d)) _dictRehashStep(d);
//计算给定键的散列值,并根据散列表的大小掩码计算索引。
h = dictHashKey(d, key);
//遍历指定索引位置上的链表,查找与给定键匹配的 dictEntry。
//如果找到匹配的键,则将其从链表中删除,并根据 nofree 参数决定是否释放内存。
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)) {
/* Unlink the element from the list */
if (prevHe)
prevHe->next = he->next;
else
d->ht[table].table[idx] = he->next;
if (!nofree) {
dictFreeKey(d, he);
dictFreeVal(d, he);
zfree(he);
}
d->ht[table].used--;
return he;
}
prevHe = he;
he = he->next;
}
if (!dictIsRehashing(d)) break;
}
return NULL; /* not found */
}
dictScan 字典扫描
dictScan
函数是 Redis 字典实现中的一个重要的遍历工具,用于扫描字典中的条目。这个函数允许用户在不锁定整个字典的情况下遍历其中的条目,这对于在高并发环境下安全地读取数据非常有用。dictScan
支持增量扫描,即可以从上次停止的地方继续扫描,这有助于减少内存消耗和提高效率。
/**
dict *d:指向 dict 结构体的指针,即字典实例。
unsigned long v:扫描的游标,表示从哪个位置开始扫描。
dictScanFunction *fn:回调函数指针,用于处理每一个遍历到的 dictEntry。
dictScanBucketFunction* bucketfn:可选的回调函数指针,用于处理每一个桶(bucket)。
void *privdata:传递给回调函数的私有数据指针。
返回值:unsigned long:新的游标值,表示下次扫描应从哪个位置开始。
**/
unsigned long dictScan(dict *d,
unsigned long v,
dictScanFunction *fn,
dictScanBucketFunction* bucketfn,
void *privdata)
{
dictht *t0, *t1;
const dictEntry *de, *next;
unsigned long m0, m1;
//如果字典没有条目,则直接返回 0。
if (dictSize(d) == 0) return 0;
/* 在扫描过程中暂停重新散列。 */
dictPauseRehashing(d);
/*如果没有正在进行的重新散列,只遍历第一个散列表*/
if (!dictIsRehashing(d)) {
t0 = &(d->ht[0]);
m0 = t0->sizemask;
/* Emit entries at cursor */
if (bucketfn) bucketfn(privdata, &t0->table[v & m0]);
de = t0->table[v & m0];
while (de) {
next = de->next;
/*调用数据处理函数*/
fn(privdata, de);
de = next;
}
/* Set unmasked bits so incrementing the reversed cursor
* operates on the masked bits */
v |= ~m0;
/* Increment the reverse cursor */
v = rev(v);
v++;
v = rev(v);
} else {
t0 = &d->ht[0];
t1 = &d->ht[1];
/* Make sure t0 is the smaller and t1 is the bigger table */
if (t0->size > t1->size) {
t0 = &d->ht[1];
t1 = &d->ht[0];
}
m0 = t0->sizemask;
m1 = t1->sizemask;
/* Emit entries at cursor */
if (bucketfn) bucketfn(privdata, &t0->table[v & m0]);
de = t0->table[v & m0];
while (de) {
next = de->next;
fn(privdata, de);
de = next;
}
/* Iterate over indices in larger table that are the expansion
* of the index pointed to by the cursor in the smaller table */
do {
/* Emit entries at cursor */
if (bucketfn) bucketfn(privdata, &t1->table[v & m1]);
de = t1->table[v & m1];
while (de) {
next = de->next;
fn(privdata, de);
de = next;
}
/* Increment the reverse cursor not covered by the smaller mask.*/
v |= ~m1;
v = rev(v);
v++;
v = rev(v);
/* Continue while bits covered by mask difference is non-zero */
} while (v & (m0 ^ m1));
}
/*扫描完成后恢复重新散列。*/
dictResumeRehashing(d);
return v;
}
dictIterator 字典迭代器
- 数据结构
typedef struct dictIterator {
dict *d; // 指向字典的指针
long index; // 当前迭代的索引位置
int table, safe; // 当前迭代的散列表编号和是否安全标志
dictEntry *entry, *nextEntry; // 当前迭代的条目和下一个条目
/* unsafe iterator fingerprint for misuse detection. */
long long fingerprint; // 不安全迭代器的指纹,用于检测误用
} dictIterator;
dict *d
:- 指向当前正在迭代的字典的指针。
long index
:- 当前迭代器所在的索引位置。这个索引用于在散列表中定位当前的桶(bucket)。
int table
:- 当前迭代的散列表编号。在字典进行重新散列时,字典会有两个散列表,
table
变量指示当前迭代的是哪一个散列表。
- 当前迭代的散列表编号。在字典进行重新散列时,字典会有两个散列表,
int safe
:- 安全标志,表示迭代器是否处于安全状态。当字典正在进行修改(如添加或删除条目)时,这个标志可以帮助迭代器决定是否继续迭代。
dictEntry *entry, *nextEntry
:- 当前迭代的条目指针和下一个条目的指针。
entry
指向当前正在处理的条目,而nextEntry
指向下一个要处理的条目。
- 当前迭代的条目指针和下一个条目的指针。
long long fingerprint
:- 不安全迭代器的指纹,用于检测迭代器的误用。这个指纹在迭代器创建时初始化,并在字典发生更改时更新。如果迭代器检测到字典的指纹与自身的指纹不匹配,则认为字典已经发生了改变,迭代器可能不再安全。
1 获取迭代器
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;
return i;
}
2 迭代数据
/*
dictEntry *:返回下一个条目的指针,如果没有更多的条目则返回 NULL
*/
dictEntry *dictNext(dictIterator *iter)
{
while (1) {
/*如果 iter->entry 为空,需要找到下一个非空的条目。*/
if (iter->entry == NULL) {
/*根据iter的table 找到正在遍历ht的那个散列表*/
dictht *ht = &iter->d->ht[iter->table];
/*如果是第一次遍历*/
if (iter->index == -1 && iter->table == 0) {
//如果是安全遍历,则加计数器。否则计算当前迭代器的hash
if (iter->safe)
dictPauseRehashing(iter->d);
else
iter->fingerprint = dictFingerprint(iter->d);
}
/*索引++*/
iter->index++;
/*当前的table遍历完了,如果是正在刷新,并且正在遍历的是ht0,则遍历ht1*/
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->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;
}
dictFind 字典查找
dictFind
函数用于在 Redis 字典中查找指定键对应的条目。这个函数会根据给定的键,利用哈希算法计算出一个哈希值,并在字典中寻找与该键匹配的条目。
/*
dict *d:指向字典的指针。
const void *key:指向要查找的键的指针
dictEntry *:返回指向找到的条目的指针,如果找不到,则返回 NULL。
*/
dictEntry *dictFind(dict *d, const void *key)
{
dictEntry *he;
uint64_t h, idx, table;
if (dictSize(d) == 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;
}
dictRehash
函数用于重新散列 Redis 字典。重新散列是指将字典中的条目从一个散列表移动到另一个散列表的过程。这个过程通常发生在字典的增长或收缩过程中,以保持散列表的有效负载因子在合理的范围内,从而维持良好的性能。
/**
dict *d:指向字典的指针。
int n:指定本次重新散列的步数,即处理的桶(buckets)数量。
**/
int dictRehash(dict *d, int n) {
int empty_visits = n*10; /* Max number of empty buckets to visit. */
/** 如果字典当前不在重新散列状态,则直接返回 0 **/
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];
/* Move all the keys in this bucket from the old to the new hash HT */
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++;
}
/* 检查是否完成重新散列 */
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;
}
4 总结
- Redis 字典为什么使用双散列表?
Redis 使用双散列表机制来支持自动扩容。当散列表的负载因子(已使用的槽位数除以总的槽位数)超过一定阈值时,Redis 会创建一个新的更大的散列表,并逐步将旧散列表中的条目迁移到新的散列表中。这样做有两个好处:首先,它可以避免在高负载下一次性重新散列整个表所带来的性能影响;其次,它确保了散列表的负载因子保持在一个合理的水平,从而保持了良好的性能。
- Redis 如何实现渐进式的重新散列?
Redis 的重新散列是一个渐进式的过程,通过
dictRehash
函数来实现。每次调用dictRehash
函数时,它只会处理一部分条目,而不是一次性处理所有的条目。这样可以将重新散列的压力分散到多次操作中,避免对系统性能造成大的冲击。
- Redis 中的迭代器是如何设计的?
Redis 的迭代器设计考虑到了安全性,特别是在遍历过程中可能发生的字典结构变化。例如,
dictNext
函数会保存当前条目的下一个条目的指针,这样即使当前条目在遍历过程中被删除,迭代器仍然可以正确地继续遍历。此外,迭代器还提供了安全模式选项,可以在遍历时暂停重新散列过程,以确保遍历的一致性。