redis数据类型-字典-rehash
redis5.8
Can Get
- 知道什么是
渐进式rehash
- 渐进式rehash的过程
- 触发rehash发生的条件
- 解决hash冲突使用的单链表的头插法
- rehash的扩容
参考链接
rehash触发(rehashidx=0)
redis用来判断是否触发rehash的函数是 _dictExpandIfNeeded
是否正在rehashing的判断函数
#define dictIsRehashing(d) ((d)->rehashidx != -1)
扩容条件
dict_can_resize = 1
dict_force_resize_ratio=5
loadFactor = d->ht[0].used / d->ht[0].size
- 条件1,ht[0]的size为0,则默认扩容大小 size=4
- 条件2,负载因子大于等于1 并且 hash表可以扩容( dict_can_resize)
- 条件3,负载因子大于5(hash冲突已经很严重,会强制的进行扩容)
- 条件[2,3]是扩容为 使用节点的2倍
/* Expand the hash table if needed */
static int _dictExpandIfNeeded(dict *d)
{
// 字典是否正在reshash中 rehahsidx != -1
if (dictIsRehashing(d)) return DICT_OK;
// ht[0]是空的, 则初始化为size=4
if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);
// loadFactor = d->ht[0].used / d->ht[0].size
// (loadFactor >=1 and (dict_can_resize || loadFactor > 5))
// 负载因子 >=1 and (dict_can_resize and 负载因子 >= 5 )
if (d->ht[0].used >= d->ht[0].size && (dict_can_resize || d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
{
// 扩充为 ht[0]节点数量的2倍
return dictExpand(d, d->ht[0].used*2);
}
return DICT_OK;
}
dict_can_resize值的修改
当redis的子进程,正在 rdb快照或者 aof重写时,dict_can_resize=0;会设置禁止改变字典大小,
// dict.h
// 启用
void dictEnableResize(void) {
dict_can_resize = 1;
}
// 禁用
void dictDisableResize(void) {
dict_can_resize = 0;
}
// serve.c
void updateDictResizePolicy(void) {
// redis没有执行,rdb快照 和 aof重写,则可以扩容
if (server.rdb_child_pid == -1 && server.aof_child_pid == -1)
dictEnableResize();
else
dictDisableResize();
}
调用关系图
redis的添加和修改都会判断是否进行rehash,调用示意图。
_dictExpandIfNeeded:会根据hash表的负载因子和dict_can_resize 是否能进行rehahs的标识,判断是否进行rehash
updateDictResizePolicy:会根据RDB和AOF的执行情况,启动和禁用rehash
rehash扩容
dict *d : 要扩容的hash表
unsigned log size :要扩容的容量的大小
以4为基数,每次以2倍的数量增加 直到 大于 size
/* Expand or create the hash table */
int dictExpand(dict *d, unsigned long size)
{
/* the size is invalid if it is smaller than the number of
* elements already inside the hash table */
if (dictIsRehashing(d) || d->ht[0].used > size)
return DICT_ERR;
dictht n; /* the new hash table */
// 根据size 返回一个realsize
unsigned long realsize = _dictNextPower(size);
/* Rehashing to the same table size is not useful. */
if (realsize == d->ht[0].size) return DICT_ERR;
/* Allocate the new hash table and initialize all pointers to NULL */
n.size = realsize;
n.sizemask = realsize-1; // size-1
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;
}
/* Prepare a second hash table for incremental rehashing */
d->ht[1] = n;
d->rehashidx = 0; // 注意这里的 rehasidx值为=0 表示rehasing开始了
return DICT_OK;
}
// 返回 realsize
static unsigned long _dictNextPower(unsigned long size)
{
unsigned long i = DICT_HT_INITIAL_SIZE; // 4
if (size >= LONG_MAX) return LONG_MAX + 1LU;
while(1) {
if (i >= size)
return i;
i *= 2;
}
}
rehash缩容
serverCron()
is called periodically (according toserver.hz
frequency), and performs tasks that must
通过一个redis的后台进程定时任务,定期的去调用 serverCron->databasesCron->tryResizeHashTables
,来检查是否需要缩容.
- tryResizeHashTables
- 字典的数量 大于 4 并且 字段使用的节点数量低于数组的长度的10%则调用 dictResize
- dictResize
- dict_can_resize(rdb没有在快照和aof页没有在重写)并且 字典没有此时没有处于 rehashing状态中。
- 则开始缩容
// dict.c
int dictResize(dict *d)
{
int minimal;
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);
}
#define dictSlots(d) ((d)->ht[0].size+(d)->ht[1].size)
#define dictSize(d) ((d)->ht[0].used+(d)->ht[1].used)
#define DICT_HT_INITIAL_SIZE 4
// server.h
#define HASHTABLE_MIN_FILL 10 /* Minimal hash table fill 10% */
// server.c
/* If the percentage of used slots in the HT reaches HASHTABLE_MIN_FILL
* we resize the hash table to save memory */
void tryResizeHashTables(int dbid) {
if (htNeedsResize(server.db[dbid].dict))
dictResize(server.db[dbid].dict);
if (htNeedsResize(server.db[dbid].expires))
dictResize(server.db[dbid].expires);
}
int htNeedsResize(dict *dict) {
long long size, used;
size = dictSlots(dict); // ht[0] + ht[1]的size的大小
used = dictSize(dict); // ht[0] + ht[1]的 used的大小
return (size > DICT_HT_INITIAL_SIZE && (used*100/size < HASHTABLE_MIN_FILL));
}
rehash实现
hash冲突解决
- redis解决hash冲突使用的是
拉链发
, 注意每次新元素插入链表时,使用的是头插法
,O(1)复杂度。
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{
/* ...... */
ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
entry = zmalloc(sizeof(*entry));
entry->next = ht->table[index];
ht->table[index] = entry;
ht->used++;
/* ...... */
return entry;
}
如果发生大量的 hash冲突,就会导致 链表的长度越长,访问复杂度最坏O(hash链表长度),严重的影响速度,所以rehash就登场了,
- rehash 简单说就是,hash表扩容,将ht[0]的元素迁移到ht[1]中,减少hash冲突,增加访问效率。
但并不是一次迁移完毕,而是,分多次,渐进式的,断续进行,这样才不会对reids的主线程造成影响。
渐进式rehash
是批量拷贝,每次只拷贝hash表中n(1)个buckt,这就对主线程的影响也就有限了。
渐进式rehash过程
-
指定允许访问最多的空bucket的数量 n*10
-
循环访问N个buckt,并且还有可用节点,复制 bucket中的节点到 ht[1]中
-
ht[0]的size 必须大于 rehahsidx
-
循环得到一个不为空的bucket
- 如果 为空的bucket 大于等于N*10,则直接return结束,避免时间过长影响服务性能。
-
将该bucket中的的节点迁移到ht[1]中bueckt中,
-
ht[0].used--,ht[1]used++
-
-
检查ht[0]中是否没有可用节点used(表示全部复制完毕了)
- 释放ht[0]空间
- 将ht[1]复制给ht[0]
- 释放ht[1]空间
- 设置rehashidx=-1,表示本次rehash结束。
// empty_visits 最大允许访问空桶的数量,超过可能会影响性能。
// n:要拷贝 bucket的数量。
int dictRehash(dict *d, int n) {
int empty_visits = n*10; /* Max number of empty buckets to visit. */ // 允许的最大的空 buckets数量。
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);
// 得到一个不为空的 bucket,
// 如果 为空数量 超过了 empty_visits ,则提前退出,避免造成性能影响。
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 */
// 将 该 bucket中 单链表 copy到 新的 ht[1]中去。
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;
}
// 完成后将 ht[0]table[rehashidx]置为nil
// rehashidx++ ,接着下一个
d->ht[0].table[d->rehashidx] = NULL;
d->rehashidx++;
}
/* Check if we already rehashed the whole table... */
// 最后在检查一下,ht[0]是否没有可用的key
if (d->ht[0].used == 0) {
zfree(d->ht[0].table); // 释放 ht[0]空间
d->ht[0] = d->ht[1]; // 将 ht[1] 赋值给 ht[0]
_dictReset(&d->ht[1]); // 重置 ht的大小为0
d->rehashidx = -1; // 设置全局hash表,rehashidx=-1,表示rehash结束
return 0; // 返回0 表示 ht[0]中的所有元素都迁移完毕
}
/* More to rehash... */
return 1; // 表示 ht[0]的元素,还没后全部迁移完毕。
}
渐进式rehash优点
分而治之的方式,将rehash的工作分派到,添加,删除,查找和更新的操作上,从而避免集中式rehash,而带来的的庞大计算。
字典的删除(delete)、查找(find)、更新(update)等操作会在两个哈希表上进行,
字典的添加(add)则只会添加到 ht[1]中去
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing){
/*....*/
ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
/*....*/
return entry;
}
// 删除,从ht[0] 和 ht[1]中去删除
static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) {
/*....*/
for (table = 0; table <= 1; table++) {
idx = h & d->ht[table].sizemask;
he = d->ht[table].table[idx];
}
/*....*/
}
dictEntry *dictFind(dict *d, const void *key){
/*....*/
for (table = 0; table <= 1; table++) {
idx = h & d->ht[table].sizemask;
he = d->ht[table].table[idx];
}
/*....*/
}
函数调用关系
_dictRehashStep调用 dictRehash 传入的循环次数都是1,
static void _dictRehashStep(dict *d) {
if (d->iterators == 0) dictRehash(d,1);
}
Q&A
rehash的触发和 渐进式rehash的有啥关系呢?
我们结合一个 【dictAddRaw】函数来分析一下
- dictIsRehashing 首先它会判断改字典 是否正处于 rehashing的状态中,
- 如果是 则进行 ht[0]数据 rehashing 到 ht[1]中 (渐进式rehash操作)
- _dictKeyIndex 找到该 新key的位置,如果需要扩容则会扩容字典,并设置 全局的 rehashidx=0
也就是说,只有 字典发生了 扩容
设置了 rehasidx=0
,则 渐进式rehash才会触发
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{
long index;
dictEntry *entry;
dictht *ht;
if (dictIsRehashing(d)) _dictRehashStep(d);
/* Get the index of the new element, or -1 if
* the element already exists. */
if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)
return NULL;
/* Allocate the memory and store the new entry.
* Insert the element in top, with the assumption that in a database
* system it is more likely that recently added entries are accessed
* more frequently. */
ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
entry = zmalloc(sizeof(*entry));
entry->next = ht->table[index];
ht->table[index] = entry;
ht->used++;
/* Set the hash entry fields. */
dictSetKey(d, entry, key);
return entry;
}