Redis的字典(dict)rehash过程源代码解析

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

Redis的字典(dict)rehash过程源代码解析
Redis的内存存储结构是个⼤的字典存储,也就是我们通常说的哈希表。

Redis⼩到能够存储⼏万记录的CACHE,⼤到能够存储⼏千万甚⾄上亿的记录(看内存⽽定),这充分说明Redis作为缓冲的强⼤。

Redis的核⼼数据结构就是字典(dict),dict在数据量不断增⼤的过程中。

会遇到HASH(key)碰撞的问题,假设DICT不够⼤,碰撞的概率增⼤,这样单个hash 桶存储的元素会越来愈多,查询效率就会变慢。

假设数据量从⼏千万变成⼏万,不断减⼩的过程。

DICT内存却会造成不必要的浪费。

Redis的dict在设计的过程中充分考虑了dict⾃⼰主动扩⼤和收缩,实现了⼀个称之为rehash的过程。

使dict出发rehash的条件有两个:
1)总的元素个数除 DICT桶的个数得到每⼀个桶平均存储的元素个数(pre_num),假设 pre_num > dict_force_resize_ratio,就会触发dict 扩⼤操作。

dict_force_resize_ratio = 5。

2)在总元素 * 10 < 桶的个数,也就是,填充率必须<10%,
DICT便会进⾏收缩。

让total / bk_num 接近 1:1。

dict rehash扩⼤流程:
源码函数调⽤和解析:
dictAddRaw->_dictKeyIndex->_dictExpandIfNeeded->dictExpand,这个函数调⽤关系是须要扩⼤dict的调⽤关系,
_dictKeyIndex函数代码:
static int _dictKeyIndex(dict *d, const void *key)
{
unsigned int h, idx, table;
dictEntry *he;
// 假设有须要。

对字典进⾏扩展
if (_dictExpandIfNeeded(d) == DICT_ERR)
return -1;
// 计算 key 的哈希值
h = dictHashKey(d, key);
// 在两个哈希表中进⾏查找给定 key
for (table = 0; table <= 1; table++) {
// 依据哈希值和哈希表的 sizemask
// 计算出 key 可能出如今 table 数组中的哪个索引
idx = h & d->ht[table].sizemask;
// 在节点链表⾥查找给定 key
// 由于链表的元素数量通常为 1 或者是⼀个⾮常⼩的⽐率
// 所以能够将这个操作看作 O(1) 来处理
he = d->ht[table].table[idx];
while(he) {
// key 已经存在
if (dictCompareKeys(d, key, he->key))
return -1;
he = he->next;
}
// 第⼀次进⾏执⾏到这⾥时,说明已经查找完 d->ht[0] 了
// 这时假设哈希表不在 rehash 其中。

就没有必要查找 d->ht[1]
if (!dictIsRehashing(d)) break;
}
return idx;
}
_dictExpandIfNeeded函数代码解析:
static int _dictExpandIfNeeded(dict *d)
{
// 已经在渐进式 rehash 其中,直接返回
if (dictIsRehashing(d)) return DICT_OK;
// 假设哈希表为空。

那么将它扩展为初始⼤⼩
// O(N)
if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);
// 假设哈希表的已⽤节点数 >= 哈希表的⼤⼩。

// ⽽且下⾯条件任⼀个为真:
// 1) dict_can_resize 为真
// 2) 已⽤节点数除以哈希表⼤⼩之⽐⼤于
// dict_force_resize_ratio
// 那么调⽤ dictExpand 对哈希表进⾏扩展
// 扩展的体积⾄少为已使⽤节点数的两倍
// O(N)
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;
}
dict rehash缩⼩流程:
源码函数调⽤和解析:
serverCron->tryResizeHashTables->dictResize->dictExpand serverCron函数是个⼼跳函数,调⽤tryResizeHashTables段为: int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) { ....
if (server.rdb_child_pid == -1 && server.aof_child_pid == -1) {
// 将哈希表的⽐率维持在 1:1 附近
tryResizeHashTables();
if (server.activerehashing) incrementallyRehash(); //进⾏rehash动作
}
....
}
tryResizeHashTables函数代码分析:
void tryResizeHashTables(void) {
int j;
for (j = 0; j < server.dbnum; j++) {
// 缩⼩键空间字典
if (htNeedsResize(server.db[j].dict))
dictResize(server.db[j].dict);
// 缩⼩过期时间字典
if (htNeedsResize(server.db[j].expires))
dictResize(server.db[j].expires);
}
}
htNeedsResize函数是推断能否够须要进⾏dict缩⼩的条件推断,填充率必须>10%,否则会进⾏缩⼩,详细代码例如以下:
int htNeedsResize(dict *dict) {
long long size, used;
// 哈希表⼤⼩
size = dictSlots(dict);
// 哈希表已⽤节点数量
used = dictSize(dict);
// 当哈希表的⼤⼩⼤于 DICT_HT_INITIAL_SIZE
// ⽽且字典的填充率低于 REDIS_HT_MINFILL 时
// 返回 1
return (size && used && size > DICT_HT_INITIAL_SIZE &&
(used*100/size < REDIS_HT_MINFILL));
}
dictResize函数代码:
int dictResize(dict *d)
{
int minimal;
// 不能在 dict_can_resize 为假
// 或者字典正在 rehash 时调⽤
if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;
minimal = d->ht[0].used;
if (minimal < DICT_HT_INITIAL_SIZE)
minimal = DICT_HT_INITIAL_SIZE;
return dictExpand(d, minimal);
}
以上两个过程终于调⽤了dictExpand函数,这个函数主要是产⽣⼀个新的HASH表(dictht),并让将dict.rehashidx= 0。

表⽰開始进⾏rehash动作。

详细的rehash动作是将ht[0]的数据依照hash隐射的规则⼜⼀次隐射到 ht[1]上.详细代码例如以下:
int dictExpand(dict *d, unsigned long size)
{
dictht n; /* 被转移数据的新hash table */
// 计算哈希表的真实⼤⼩
unsigned long realsize = _dictNextPower(size);
if (dictIsRehashing(d) || d->ht[0].used > size || d->ht[0].size == realsize)
return DICT_ERR;
// 创建并初始化新哈希表
n.size = realsize;
n.sizemask = realsize-1;
n.table = zcalloc(realsize*sizeof(dictEntry*));
ed = 0;
// 假设 ht[0] 为空,那么这就是⼀次创建新哈希表⾏为
// 将新哈希表设置为 ht[0] ,然后返回
if (d->ht[0].table == NULL) {
d->ht[0] = n;
return DICT_OK;
}
/* Prepare a second hash table for incremental rehashing */
// 假设 ht[0] 不为空。

那么这就是⼀次扩展字典的⾏为
// 将新哈希表设置为 ht[1] ,并打开 rehash 标识
d->ht[1] = n;
d->rehashidx = 0;
return DICT_OK;
}
字典dict的rehashidx被设置成0后,就表⽰開始rehash动作,在⼼跳函数运⾏的过程,会检查到这个标志,假设须要rehash,即可进⾏渐进式rehash动作。

函数调⽤的过程为:
serverCron->incrementallyRehash->dictRehashMilliseconds->dictRehash
incrementallyRehash函数代码:
/*
* 在 Redis Cron 中调⽤,对数据库中第⼀个遇到的、能够进⾏ rehash 的哈希表
* 进⾏ 1 毫秒的渐进式 rehash
*/
void incrementallyRehash(void) {
int j;
for (j = 0; j < server.dbnum; j++) {
/* Keys dictionary */
if (dictIsRehashing(server.db[j].dict)) {
dictRehashMilliseconds(server.db[j].dict,1);
break; /* 已经耗尽了指定的CPU毫秒数 */
}
...
}
dictRehashMilliseconds函数是依照指定的CPU运算的毫秒数,运⾏rehash动作,每次⼀个100个为单位运⾏。

代码例如以下:
/*
* 在给定毫秒数内,以 100 步为单位,对字典进⾏ rehash 。

*/
int dictRehashMilliseconds(dict *d, int ms) {
long long start = timeInMilliseconds();
int rehashes = 0;
while(dictRehash(d,100)) {/*每次100步数据*/
rehashes += 100;
if (timeInMilliseconds()-start > ms) break; /*耗时完成。

暂停rehash*/
}
return rehashes;
}
/*
* 运⾏ N 步渐进式 rehash 。

*
* 假设运⾏之后哈希表还有元素须要 rehash 。

那么返回 1 。

* 假设哈希表⾥⾯全部元素已经迁移完成,那么返回 0 。

*
* 每步 rehash 都会移动哈希表数组内某个索引上的整个链表节点,
* 所以从 ht[0] 迁移到 ht[1] 的 key 可能不⽌⼀个。

*/
int dictRehash(dict *d, int n) {
if (!dictIsRehashing(d)) return 0;
while(n--) {
dictEntry *de, *nextde;
// 假设 ht[0] 已经为空,那么迁移完成
// ⽤ ht[1] 取代原来的 ht[0]
if (d->ht[0].used == 0) {
// 释放 ht[0] 的哈希表数组
zfree(d->ht[0].table);
// 将 ht[0] 指向 ht[1]
d->ht[0] = d->ht[1];
// 清空 ht[1] 的指针
_dictReset(&d->ht[1]);
// 关闭 rehash 标识
d->rehashidx = -1;
// 通知调⽤者, rehash 完成
return 0;
}
assert(d->ht[0].size > (unsigned)d->rehashidx);
// 移动到数组中⾸个不为 NULL 链表的索引上
while(d->ht[0].table[d->rehashidx] == NULL) d->rehashidx++;
// 指向链表头
de = d->ht[0].table[d->rehashidx];
// 将链表内的全部元素从 ht[0] 迁移到 ht[1]
// 由于桶内的元素通常仅仅有⼀个,或者不多于某个特定⽐率
// 所以能够将这个操作看作 O(1)
while(de) {
unsigned int h;
nextde = de->next;
/* Get the index in the new hash table */
// 计算元素在 ht[1] 的哈希值
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;
}
// 设置指针为 NULL ,⽅便下次 rehash 时跳过
d->ht[0].table[d->rehashidx] = NULL;
// 前进⾄下⼀索引
d->rehashidx++;
}
// 通知调⽤者,还有元素等待 rehash
return 1;
}
总结。

Redis的rehash动作是⼀个内存管理和数据管理的⼀个核⼼操作,因为Redis主要使⽤单线程做数据管理和消息效应。

它的rehash数据迁移过程採⽤的是渐进式的数据迁移模式。

这样做是为了防⽌rehash过程太长阻塞数据处理线程。

并没有採⽤memcached的多线程迁移模式。

关于memcached的rehash过程,以后再做介绍。

从redis的rehash过程设计的⾮常巧,也⾮常优雅。

在这⾥值得注意的是,redis在find数据的时候,是同⼀时候查找正在迁移的ht[0]和被迁移的ht[1]。

防⽌迁移过程数据命不中的问题。

相关文档
最新文档