这是我参与8月更文挑战的第20天,活动详情查看:8月更文挑战
redis 提供了经典的 hashtable 设计:
- hash 冲突 ⇒ 拉链法
- 扩容 ⇒ rehash 扩容。准备两个 hash,交替工作
其中扩容这个,在 go map 改造中可以借用,可以有效防止 delete map key 造成的内存泄漏。
hash 冲突
先说说 redis 存储 dict 的结构:
typedef struct dictht {
dictEntry **table; // table array 每一个都指向 dictEntry
unsigned long size; // Hash表大小
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;
- dict 中含有若干个桶 → dictEntry
- 每一个桶的指针,带有 next 指针 → 实现链式hash的关键
hash 冲突就是使用 next 将node指针串联在一个桶中。那么当链表长度变长,查询效率变慢,有什么方法可以减少对 hashtable 的查询影响呢?
rehash
一句话:就是扩大 Hash 表空间。基本思路如下:
- 准备两个 hashtable,在 rehash 中交替存储数据
- 正常响应请求 → 写入 hash[0]
- rehash →
hash[0] ⇒ hash[1]
- reahash 迁移完毕 → hash[0] 空间释放,然后 hash[1] ⇒ hash[0] (指针指向转换),hash[1] 大小空间置为 0
而 redis 的 rehash 方式是:渐进式hash,这也是性能的体现。那为什么要实现渐进式 rehash?
键拷贝过程中,redis 主线程无法执行其他要求,阻塞主线程,加大 rehash 的开销
简单来说:把很大块迁移数据的开销,平摊到多次小的操作中,目的是降低主线程的性能影响
具体操作就是:
每次的键拷贝,只拷贝 hashtable 中的一个 bucket 对应的 dictEntry
关键函数:dictRehash && _dictRehashStep
先来看看 dictRehash
:
int dictRehash(dict *d, int n) {
int empty_visits = n*10;
...
// 主循环,根据要拷贝的bucket数量n,循环n次后停止或ht[0]中的数据迁移完停止
while(n-- && d->ht[0].used != 0) {
// 如果当前要迁移的bucket中没有元素
while(d->ht[0].table[d->rehashidx] == NULL) {
d->rehashidx++;
if (--empty_visits == 0) return 1;
}
...
// 获得哈希表中哈希项
de = d->ht[0].table[d->rehashidx];
// 如果rehashidx指向的bucket不为空
while(de) {
uint64_t h;
// 获得同一个bucket中下一个哈希项
nextde = de->next;
// 根据扩容后的哈希表ht[1]大小,计算当前哈希项在扩容后哈希表中的bucket位置
h = dictHashKey(d, de->key) & d->ht[1].sizemask;
// 将当前哈希项添加到扩容后的哈希表ht[1]中
de->next = d->ht[1].table[h];
d->ht[1].table[h] = de;
// 减少当前哈希表的哈希项个数
d->ht[0].used--;
// 增加扩容后哈希表的哈希项个数
d->ht[1].used++;
// 指向下一个哈希项
de = nextde;
}
// 如果当前bucket中已经没有哈希项了,将该bucket置为NULL
d->ht[0].table[d->rehashidx] = NULL;
// 将rehash加1,下一次将迁移下一个bucket中的元素
d->rehashidx++;
}
// ht[0] 是否迁移完毕
if (d->ht[0].used == 0) {
// 迁移完毕,释放空间
zfree(d->ht[0].table);
// 让ht[0]指向ht[1],ht[0] 继续接受正常请求
d->ht[0] = d->ht[1];
// 重置ht[1]的大小为0
_dictReset(&d->ht[1]);
// 设置全局哈希表的rehashidx标识为-1,表示rehash结束
d->rehashidx = -1;
// 返回0,表示ht[0]中所有元素都迁移完
return 0;
}
// 返回1,表示ht[0]中仍然有元素没有迁移完
return 1;
}
核心:迁移某一个 bucket 下的全部 hash entry,步骤如下:
- 根据 rehashidx (当前的迁移进度) 确定到 bucket
- 如果 bucket 还有 next hash entry,继续迁移
- 先保存在 ht[0] 中找到的 bucket 中的第一个 hash entry→next,作为下一次循环的开始 (其实是要保存在 ht[0] 中的迁移的指针,不然下一个需要迁移的 entry 哪里找)
- 根据扩容,找到在 ht[1] 中新的 bucket 所在位置
- 链表插入
- ht[0] - 1,ht[1] + 1
- 指向下一个哈希项 (ht[0] 的next )