在 Redis 中,字典(或哈希表)是其内部数据结构的基础之一。Redis 使用字典来实现键值对存储、集合、哈希、ZSET 等数据类型。理解 Redis 的字典设计和实现有助于更好地掌握 Redis 的性能和工作原理。
字典的定义
Redis 的字典主要由两个结构体定义:dict 和 dictEntry。以下是它们的简化结构:
dictEntry 结构
每个 dictEntry 是一个键值对条目,包括指向下一个条目的指针(用于解决哈希冲突)。
typedef struct dictEntry {
void *key; // 键
union {
void *val; // 值
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next; // 链接到下一个条目,用于冲突链表
} dictEntry;
dict 结构
dict 是字典的主体,包含哈希表和其他必要的信息。
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]; // 两个哈希表,用于渐进式 rehash
long rehashidx; // 重新哈希的进度,如果没有进行则为 -1
unsigned long iterators; // 当前运行的迭代器数量
} dict;
哈希算法
Redis 使用了 MurmurHash2 算法进行哈希计算,这是一种速度快、散列均匀的哈希算法。
冲突解决
当发生哈希冲突时,Redis 使用链地址法(Separate Chaining),即通过链表将多个键值对存储在同一个槽中。
渐进式 Rehashing
为了避免在一次性扩展或收缩哈希表时可能带来的延迟,Redis 采用了渐进式 rehashing 技术。rehashing 过程如下:
- 触发条件:当哈希表中的键值对数量达到一定阈值时,Redis决定需要扩展或收缩哈希表。
- 分配新表:Redis创建一个新的哈希表,这个表的大小是当前哈希表大小的两倍或更小(用于收缩)。
- 标记迁移状态:在字典结构中标记正在进行渐进式Rehash,并记录当前迁移的位置(通常从0开始)。
- 渐进式迁移:每次对哈希表进行读写操作时(如查询、插入或删除),Redis不仅执行该操作,还会顺便将旧哈希表中的部分键值对迁移到新哈希表中。具体的迁移量取决于操作的复杂度和系统负载,迁移单位:bucket
- 更新位置:每次迁移后,更新当前迁移的位置指针,指向下一个待迁移的区域。
- 完成迁移:当所有键值对都迁移完毕后,Redis用新哈希表替换旧哈希表,并结束渐进式Rehash状态。
优点
- 避免卡顿:通过将Rehash过程分散到多个操作中,避免了一次性大规模迁移可能带来的系统卡顿。
- 保持响应性能:即使在进行Rehash期间,系统也可以处理其他请求,保持高响应性能。
- 渐进迁移:逐步迁移确保每次操作只需少量额外开销,不会显著影响系统整体性能。
字典操作
基本的字典操作包括插入、查找和删除。这些操作在 Redis 源码的 dict.c 文件中实现。以下是一些关键函数的简要说明:
插入操作
插入一个键值对到字典中:
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) {
/* ...检查是否需要 rehash,并进行相应操作... */
/* ...计算键的哈希值,并找到对应的槽位... */
entry = zmalloc(sizeof(*entry));
entry->next = d->ht[0].table[index];
d->ht[0].table[index] = entry;
d->ht[0].used++;
dictSetKey(d, entry, key);
return entry;
}
查找操作
根据键查找对应的值:
dictEntry *dictFind(dict *d, const void *key) {
unsigned int h, idx, table;
dictEntry *he;
if (d->ht[0].size == 0) return NULL; /* 空字典 */
if (dictIsRehashing(d)) _dictRehashStep(d); /* 如果正在进行 rehash,则执行单步 rehash */
for (table = 0; table <= 1; table++) {
h = dictHashKey(d, key);
idx = h & d->ht[table].sizemask;
he = d->ht[table].table[idx];
while(he) {
if (dictCompareKeys(d, key, he->key))
return he;
he = he->next;
}
if (!dictIsRehashing(d)) return NULL; /* 没有找到且没有在 rehash,提前返回 */
}
return NULL; /* 没有找到 */
}
删除操作
从字典中删除一个键值对:
int dictDelete(dict *d, const void *key) {
return dictGenericDelete(d, key, 1);
}
static int dictGenericDelete(dict *d, const void *key, int nofree) {
unsigned int h, idx, table;
dictEntry *he, *prevHe;
if (d->ht[0].size == 0) return DICT_ERR; /* 空字典 */
if (dictIsRehashing(d)) _dictRehashStep(d);
for (table = 0; table <= 1; table++) {
h = dictHashKey(d, key);
idx = h & d->ht[table].sizemask;
he = d->ht[table].table[idx];
prevHe = NULL;
while (he) {
if (dictCompareKeys(d, key, he->key)) {
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 DICT_OK;
}
prevHe = he;
he = he->next;
}
if (!dictIsRehashing(d)) break;
}
return DICT_ERR; /* 没有找到 */
}
渐进式 Rehash
渐进式Rehash的触发
Redis决定是否需要Rehash主要通过以下函数:
int _dictExpandIfNeeded(dict *d) {
// 如果正在进行Rehash,直接返回
if (dictIsRehashing(d)) return DICT_OK;
// 如果负载因子超过阈值,则触发扩展
if (d->ht[0].used >= d->ht[0].size && (d->ht[0].used / d->ht[0].size > HASHTABLE_MAX_LOAD_FACTOR)) {
return dictExpand(d, d->ht[0].size * 2);
}
return DICT_OK;
}
初始化Rehash
当决定进行Rehash时,会分配一个新的哈希表,并开始迁移:
int dictExpand(dict *d, unsigned long size) {
// 分配新哈希表
dictht n;
unsigned long realsize = _dictNextPower(size);
n.size = realsize;
n.sizemask = realsize - 1;
n.table = zcalloc(realsize * sizeof(dictEntry*));
n.used = 0;
// 将新表赋值给ht[1]
d->ht[1] = n;
d->rehashidx = 0; // 开始进行Rehash
return DICT_OK;
}
渐进式数据迁移
在每次对哈希表进行操作时,会顺便迁移一些数据:
void _dictRehashStep(dict *d) {
if (d->iterators == 0) dictRehash(d, 1); // 每次迁移一个bucket
}
int dictRehash(dict *d, int n) {
if (!dictIsRehashing(d)) return 0;
while (n--) {
dictEntry *de, *nextde;
// 如果所有数据都迁移完毕
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;
}
// 找到第一个非空bucket
while (d->ht[0].table[d->rehashidx] == NULL) d->rehashidx++;
de = d->ht[0].table[d->rehashidx];
while(de) {
nextde = de->next;
// 重新计算键在新表中的位置,并插入
unsigned int 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++;
}
return 1;
}
结束Rehash
当所有数据都迁移完毕后,需要将新哈希表替换旧哈希表,并重置相关状态:
if (d->ht[0].used == 0) {
zfree(d->ht[0].table);
d->ht[0] = d->ht[1];
_dictReset(&d->ht[1]);
d->rehashidx = -1;
}