《Redis设计与实现》之第四章:字典
一,哈希表节点
哈希表节点使用dictEntry结构表示,每个dictEntry结构都保存一个键值对。
typedef struct dictEntry{
//键
void *key;
//值
union{
void *val;
uint64_t u64;
int64_t s64;
} v;
//指向下一个哈希表节点,形成链表
struct dictEntry *next;
}
二,哈希表
Redis的字典所使用的哈希表由dictht结构定义:
typedef struct dictht{
//哈西表数组
dictEntry **tables;
//哈希表大小
unsigned long size;
//哈希表大小掩码,用来计算索引值,总是等于size-1
unsigned long sizemask;
//哈希表已经有的节点数量
unsigned long used;
}
table是一个数组,数组中的每个元素都是一个指向dictEntry结构的指针,每个dictEntry结构保存者一个键值对。
size记录了哈希表的大小,即table数组的大小
used记录了哈希表目前已经有的节点(键值对)的数量
sizemask和哈希值决定一个键应该被放到table数组的哪个索引上(索引值 = hash值 / sizemask)
三,字典
1. Redis字典使用的哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,每个哈希表节点保存一个键值对。
Redis中的字典由dict结构表示:
typedef struct dict{
//特定类型函数
dictType *type;
//私有数据
void *privdata;
//哈希表
dictht ht[2];
//rehash索引,当rehash不在进行时,值为-1
int rehashidx;
}
type是一个指向dictType结构的指针,每个dictType结构保存了一些用于操作特定类型键值对的函数,Redis会为不同用途的字典设置不同类型的函数
privdata保存了特定类型函数的参数
type属性和privdata属性时针对不同类型的键值对,为创建多态字典而设置的
typedef struct dictType{
//计算哈希值的函数
unsigned int (*hashFunction)(const viod *key)
//复制键的函数
//复制值的函数
//销毁键的函数
//销毁值的函数
}
ht属性时一个包含两个项的数组,数组中每个项都是一个dictht哈希表,一般情况下,字典只使用ht[0]哈希表,ht[1]哈希表只会对ht[0]哈希表rehash时才使用
rehashidx记录了rehash目前的进度,如果目前没有在进行rehash,那么它的值为-1
2. 哈希算法
当把一个新的键值对添加到字典中时,程序先根据键值对的键计算出哈希值,然后计算出索引值,最后把包含新键值对的哈希表节点放到哈希表数组的指定索引上。
使用字典设置的哈希函数,计算key的哈希值。 hash = dict -->type --> hashFunction(key);
使用哈希表的sizemask属性和哈希值,计算出索引值。 index = hash & dict --> ht[x].sizemask;
3.解决键冲突
当有两个或两个以上数量的键被分配到了哈希表数组的同一个索引上面时,我们称这些键发生了冲突。Redis的哈希表使用链地址法来解决键冲突
4.rehash
为了让哈希表的负载因子维持在一个合理的范围之内,当哈希表保存的键值对数量太多或太少时,程序需要对哈希表的大小进行相应的扩展或收缩。扩展和收缩哈希表的工作可以通过rehash(重新散列)操作来完成。
5.渐进式rehash
进行rehash操作时,需要将ht[0]中所有的键值对rehash到ht[1]上。如果哈希表中有大量的数据,只用一次rehash完成操作的话,会非常消耗服务器的性能,导致服务器在一段时间内停止服务。那么如何避免对服务器造成影响呢? 这就需要渐进式rehash了。
在rehash进行期间,每次对字典执行添加,删除,查找,或更新操作时,程序除了执行指定的操作外,还会顺带执行一次rehash操作(将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1]上,rehash完成后,将rehashidx值增加一)。我们每次只rehash 哈希表ht[0]的某一个索引上的键值对。