REDIS_HASH
Hash本质上就是一个保存若干键值对的数据结构,类似于Java中的HashMap。
同样的,hash中只能存在一个独一无二的key,所有的操作都围绕key展开。
hash的最大优点在于其可以提供最佳O(1)
的查询时间复杂度。
通过一段原始数据key,通过特定算法将其哈希值转化为数组下标,通过相同的算法处理相同的值可以计算相同的索引,所以只需要O(1)时间复杂度就可以查询到key。
但有一片阴云一直笼罩在哈希表之上:哈希冲突。
哈希冲突
前文提到,哈希表通过算法将key转化为下标,可以做到相同key,一定能有相同的下标值。
但key是无限的,下标值是有限的。无限对有限的映射,则必定有不同的key指向相同的下标,两个key抢占一个下标,就造成了哈希冲突。
哈希冲突一般有多重解决方法,Redis采用链式哈希解决。
简单来说,链式哈希遇到哈希冲突,则将数组元素转化为一个链表,通过链表将冲突的元素连接起来。
在链表中,查询时间复杂度为O(n)
,这也是为什么要说hash最佳时间复杂度为O(1)。
结构设计
typedef struct dictht {
// hash数组
dictEntry **table;
// hash大小
unsigned long size;
// 大小掩码,用于计算索引值
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;
在dictEntry的v中,是一个联合体union,里面存放一个指针,int和double。
这样的好处是如果值是一个整数或者小数,可以直接嵌入在dictEntry中而不需要指针。
rehash操作
typedef struct dict {
…
// 两个Hash表,交替使用,用于rehash操作
dictht ht[2];
…
} dict;
在正常服务请求阶段,插入的数据,都会写入到哈希表 1
,此时的哈希表 2
并没有被分配空间。
随着数据逐步增多,触发了 rehash 操作,这个过程分为三步:
- 给表 2 分配空间,一般会是表 1 的 2 倍;
- 将表 1 的数据迁移到表 2 中;
- 迁移完成后,释放表 1 的空间,并把表 2设置为表 1,然后在表 2 新创建一个空白的表,为下次 rehash 做准备。
但如果表 1 中数据量非常大,那么在迁移操作时会耗费大量计算资源,可能会阻塞Redis正常业务。
为了避免这种情况,Redis提出 渐进式哈希 作为解决方案。
渐进式哈希的核心是将数据迁移操作分步进行,而不是一口气完成。
在给表 2 分配空间后,每次哈希表的元素进行操作(增删改查)时,Redis会将被操作元素索引上的所有元素(因为可能是链表)迁移到表2上。
随着操作越来越多,终有一刻表 1 能把所有数据都迁移到表 2 上,完成渐进式哈希操作。
何时处罚rehash
我们首先要明白一个概念:Load Factor(负载因子)。
负载因子 = 哈希表已存节点数 / 哈希表大小
以下两种条件满足一种,哈希表就会进行rehash操作:
- 当负载因子大于等于 1 ,并且 Redis 没有执行 RDB 快照或没有进行 AOF 重写的时候,就会进行 rehash 操作。
- 当负载因子大于等于 5 时,此时说明哈希冲突非常严重了,不管有没有有在执行 RDB 快照或 AOF 重写,都会强制进行 rehash 操作。