Redis - rehash扩容分析
这篇文章主要分享的是 redis中的 rehash机制,结合着redis源码来学习
用过redis的朋友们都知道 redis是一个 KV类型的键值数据库,一个key 对应一个value
比如:
set testkey testvalue
键testkey :对应值testvalue
这种一个key 对应一个value的结构组织其实是redis的索引模块,redis的索引模块是使用hash表(散列表)实现的
索引模块
一个hash表其实就是一个数组,数组每个元素称为一个hash桶,一个hash表由多个hash桶组成,每个hash桶中保存键值对数据
hash冲突
当redis写入大量数据的时候就会有一个风险点,就是hash冲突 表示多个key被分配到了一个hash桶中
如何解决hash冲突?
redis会通过链表的方式来解决冲突,就是将冲突的数据以链表的方式连接起来对应桶的位置
查询的时候通过hash值定位到hash桶的位置后遍历链表进行查询
为什么要 rehash(扩容)?
随着写入的数据越来越多,冲突就会增多,链表也会越来越长,进而导致查询冲突链上的内容会耗时长,效率低
redis为了解决这个问题会对hash表做 rehash操作(hash表扩容),这个过程分三步:
1、创建一个新hash表2,给hash表2分配更大的空间,新hash表的大小至少是旧hash表1已使用节点数的两倍
2、把hash表1 中的数据重新映射并拷贝到hash表2中
3、释放hash表1的空间
渐进式 rehash
到这里完成了hash表的扩容,但是这里有个问题,如果一次性把hash表1中的数据都迁移完,就会造成redis线程阻塞,无法处理其他服务请求,为了避免这个问题,redis使用渐进式 rehash
这里概括一下什么是渐进式rehash,就是每处理一个请求时,从hash表1中的第一个索引位置开始,顺带着将这个索引位置上的所有内容拷贝到hash表2中,等处理下一个请求时,再顺带拷贝hash表1中的下一个索引为的内容
上源码:源码1
/* Performs N steps of incremental rehashing. Returns 1 if there are still
* keys to move from the old to the new hash table, otherwise 0 is returned.
*
* 执行 N 步渐进式 rehash 。
*
* 返回 1 表示仍有键需要从 0 号哈希表移动到 1 号哈希表,
* 返回 0 则表示所有键都已经迁移完毕。
*
* Note that a rehashing step consists in moving a bucket (that may have more
* than one key as we use chaining) from the old to the new hash table.
*
* 注意,每步 rehash 都是以一个哈希表索引(桶)作为单位的,
* 一个桶里可能会有多个节点,
* 被 rehash 的桶里的所有节点都会被移动到新哈希表。
*
* T = O(N)
*/
int dictRehash(dict *d, int n) {
// 只可以在 rehash 进行中时执行
if (!dictIsRehashing(d)) return 0;
// 进行 N 步迁移
// T = O(N)
while(n--) {
dictEntry *de, *nextde;
/* Check if we already rehashed the whole table... */
// 如果 0 号哈希表为空,那么表示 rehash 执行完毕
// T = O(1)
if (d->ht[0].used == 0) {
// 释放 0 号哈希表
zfree(d->ht[0].table);
// 将原来的 1 号哈希表设置为新的 0 号哈希表
d->ht[0] = d->ht[1];
// 重置旧的 1 号哈希表
_dictReset(&d->ht[1]);
// 关闭 rehash 标识
d->rehashidx = -1;
// 返回 0 ,向调用者表示 rehash 已经完成
return 0;
}
/* Note that rehashidx can't overflow as we are sure there are more
* elements because ht[0].used != 0 */
// 确保 rehashidx 没有越界
assert(d->ht[0].size > (unsigned)d->rehashidx);
// 略过数组中为空的索引,找到下一个非空索引
while(d->ht[0].table[d->rehashidx] == NULL) d->rehashidx++;
// 指向该索引的链表表头节点
de = d->ht[0].table[d->rehashidx];
/* Move all the keys in this bucket from the old to the new hash HT */
// 将链表中的所有节点迁移到新哈希表
// T = O(1)
while(de) {
unsigned int 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;
// 更新 rehash 索引
d->rehashidx++;
}
return 1;
}什么时候 rehash?
以下两个条件之一为真时,对字典进行扩展
1)字典已使用节点数和字典大小之间的比率接近 1:1
并且 dict_can_resize 为真
2)已使用节点数和字典大小之间的比率超过 dict_force_resize_ratio
dict_force_resize_ratio在源码里 默认为 5
上源码:源码2
// 指示字典是否启用 rehash 的标识
static int dict_can_resize = 1;
// 强制 rehash 的比率
static unsigned int dict_force_resize_ratio = 5;
/*
* 根据需要,初始化字典(的哈希表),或者对字典(的现有哈希表)进行扩展
*
* T = O(N)
*/
static int _dictExpandIfNeeded(dict *d)
{
/* Incremental rehashing already in progress. Return. */
// 渐进式 rehash 已经在进行了,直接返回
if (dictIsRehashing(d)) return DICT_OK;
/* If the hash table is empty expand it to the initial size. */
// 如果字典(的 0 号哈希表)为空,那么创建并返回初始化大小的 0 号哈希表
// T = O(1)
if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);
/* If we reached the 1:1 ratio, and we are allowed to resize the hash
* table (global setting) or we should avoid it but the ratio between
* elements/buckets is over the "safe" threshold, we resize doubling
* the number of buckets. */
// 一下两个条件之一为真时,对字典进行扩展
// 1)字典已使用节点数和字典大小之间的比率接近 1:1
// 并且 dict_can_resize 为真
// 2)已使用节点数和字典大小之间的比率超过 dict_force_resize_ratio
if (d->ht[0].used >= d->ht[0].size &&
(dict_can_resize ||
d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
{
// 新哈希表的大小至少是目前已使用节点数的两倍
// T = O(N)
return dictExpand(d, d->ht[0].used*2);
}
return DICT_OK;
}会不会存在hash表3?
有一个小可爱提出了这个问题,如果hash表2满了会不会创建hash表3?这里我们来一起分析一下
首先 渐进式rehash是在每次执行查询和更新操作的时候将hash表1中的一个hash桶内的数据同步分配到hash表2中,在源码1中有展示
hash表扩展的规则是扩展后的大小是扩展前的2倍
所以说 根据渐进式rehash的规则当hash表2满了的时候 都够做同步hash表1的2轮操作了
得出的结论是 rehash只会有两张hash表交替使用
上源码:源码3
/* This function performs just a step of rehashing, and only if there are
* no safe iterators bound to our hash table. When we have iterators in the
* middle of a rehashing we can't mess with the two hash tables otherwise
* some element can be missed or duplicated.
*
* 在字典不存在安全迭代器的情况下,对字典进行单步 rehash 。
*
* 字典有安全迭代器的情况下不能进行 rehash ,
* 因为两种不同的迭代和修改操作可能会弄乱字典。
*
* This function is called by common lookup or update operations in the
* dictionary so that the hash table automatically migrates from H1 to H2
* while it is actively used.
*
* 这个函数被多个通用的查找、更新操作调用,
* 它可以让字典在被使用的同时进行 rehash 。
*
* T = O(1)
*/
static void _dictRehashStep(dict *d) {
if (d->iterators == 0) dictRehash(d,1);
}