简述
在redis的字典(dict.h)实现中,当哈希表保存的键值对太多或者太少时,会触发扩展/收缩;
- 触发收缩:负载因子小于 0.1
- 触发扩展:以下任一条件符合即可
- 服务器目前没有在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 1
- 服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 5
# 负载因子 = 哈希表已保存节点数量 / 哈希表大小
load_factor = ht[0].used / ht[0].size
由于存在表中的键值对可能有成百上千个,一次性rehash到ht[1]的话会导致服务器在一段时间内停止服务,于是出现了渐进式rehash,这个动作并不是一次性的,而是分多次,渐进式完成的
机制
- 为ht[1]分配空间,此时字典会持有ht[0]和ht[1]两个hash表(rehash期间的内存变化如下,可以看到rehash后内存有了较大的上升,当rehash结束后会释放ht[0]的内存,则内存占用会下降;注意:若是扩展前,本机内存不够了,则会触发满容驱逐淘汰)
- 将dict结构体中的rehashidx置为0,表示rehash开始
/* 查看字典是否正在 rehash */
#define dictIsRehashing(ht) ((ht)->rehashidx != -1)
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];
// rehash 索引,使用宏dictIsRehashing判断是否在rehash
// 当 rehash 不在进行时,值为 -1
// _dictInit置为-1, dictExpand时置为0,dictRehash中当ht[0].used==0即rehash完成时置为-1
int rehashidx;
int iterators;
} dict;
-
rehash期间,dict的操作如下
- 增:仅添加在ht[1]中
- 删:从ht[0]中找到了则删除ht[0],如果找不到则到ht[1]删
- 查:先从ht[0]中查,查不到再从ht[1]中查
- 改:先查(如上,会查找两个表),再修改节点
-
为了防止有的KV长时间不访问一直不迁移到ht[1],会定时去rehash,每次迁移100个(如下)
int dictRehashMilliseconds(dict *d, int ms) {
// 记录开始时间
long long start = timeInMilliseconds();
int rehashes = 0;
// 每次迁移100个
while(dictRehash(d,100)) {
rehashes += 100;
// 如果时间已过,跳出
if (timeInMilliseconds()-start > ms) break;
}
return rehashes;
}
- 字典操作不断执行后,最终ht[0]键值对都会被rehash到ht[1],将rehashidx置为-1并释放ht[0]
参考
书籍:redis设计与实现