字典又称为符号表或者关联数组、或映射(map),是一种用于保存键值对的抽象数据结构。字典中的每一个键 key 都是唯一的,通过 key 可以对值来进行查找或修改。C 语言中没有内置这种数据结构的实现,所以字典依然是 Redis自己构建的。
Redis的字典是用哈希表作为底层实现的,一个哈希表中可以有多个哈希表节点,每个哈希表节点保存字典中的一个键值对。
哈希表结构
typedef struct dictht{
dictEntry **table;//哈希表数组
unsigned long size;//哈希表大小
unsigned long sizemask;//哈希表大小掩码,用于计算索引值,总是等于size-1
unsigned long used;//该哈希表已有的节点数量
} dictht;
哈希表节点
typedef struct dictEntry{
void *key;//键
union{//值
void *val;
uint64_t u64;
int64_t s64;
} v;
struct dictEntry *next;//指向下个哈希表节点,形成链表
} dictEntry;
next属性是指向另一个哈希表节点的指针,作用是将多个哈希值相同的键值对连接在一起,形成链表(如下图)。
3、字典
typedef struct dict{
dictType *type;//类型特定函数
void *privdata;//私有数据
dictht ht[2];//哈希表
int rehashidx;//rehash索引,当rehash不在进行时,值为-1
} dict;
type 和privdata属性是针对不同类型的键值对,为创建多态字典而设置的。
ht属性是一个包含两个dicyht哈希表的数组,一般情况下,字典只使用ht[0]哈希表,ht[1]哈希表只会在对ht[0]哈希表进行rehash时使用。
rehashidx属性记录了rehash目前的进度,如果目前没有进行rehash,那么其值为-1
4、哈希算法
当要将一个新的键值对添加到字典中时,需要根据键计算出哈希值和索引值,然后根据索引值将包含新键值对的哈希表节点放到哈希表数组的指定索引上。
哈希值:
hash = dict->type->hashFunction(key) //使用字典设置的hashFunction函数计算key的哈希值
索引值:
index = hash & dict->ht[x].sizemask //使用哈希值和sizemask计算索引值,ht[x]根据情况不同可以是ht[0]或ht[1]
5、解决键冲突
类似hashmap使用链地址法解决冲突。
新节点添加到链表的表头位置(头插法)
6、rehash
随着操作的不断执行,键值对会逐渐增多或减少,为了将负载因子维持在合理范围,需要对哈希表大小进行相应的扩展或收缩。
扩展或收缩通过rehash操作来完成,步骤如下:
(1)为ht[1]哈希表分配空间,空间大小取决于ht[0].used属性的值
若是扩展操作,那么ht[1]的大小为第一个>=ht[0].used*2的2^n (如果ht[0].used=4,那么4乘2=8,而8恰好是第一个>=4的2的n次方,那么ht[1]的大小就应该分配为8)
若是收缩操作,那么ht[1]的大小为第一个>=ht[0].used的2^n
(2)将保存在ht[0]中的所有键值对rehash到ht[1]中,rehash指重新计算键的哈希值和索引值,然后将键值对放置到ht[1]哈希表的指定位置上。
(3)当ht[0]的所有键值对都迁移到了ht[1]之后(ht[0]变为空表),释放ht[0],将ht[1]设置为ht[0],新建空白的哈希表ht[1],以备下次rehash使用
扩展与收缩的条件
(1)当以下条件满足任意一个时,程序就会对哈希表进行扩展操作
服务器目前没有执行bgsave或bgrewriteaof命令,并且哈希表的负载因子>=1
服务器目前正在执行bgsave或bgrewriteaof命令,并且哈希表的负载因子>=5
(2)当负载因子的值小于0.1时,程序就会对哈希表进行收缩操作
负载因子的计算
load_factor = ht[0].used / ht[0].size
7、渐进式rehash
扩展或收缩哈希表需要将 ht[0] 里面的所有键值对 rehash 到 ht[1] 里面, 但是, 这个 rehash 动作并不是一次性、集中式地完成的, 而是分多次、渐进式地完成的。
原因在于如果键值对的数量太多的话,要一次性将所有键值对全部rehash到ht[1]中的话,庞大的计算量可能会导致服务器再一段时间内停止服务。
渐进式rehash的步骤
为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表
维持索引计数器变量rehashidx,并将它的值设置为0,表示rehash开始
每次对字典执行增删改查时,将ht[0]的rehashidx索引上的所有键值对rehash到ht[1],将rehashidx值+1。
当ht[0]的所有键值对全部被rehash到ht[1]中,程序将rehashidx的值设置为-1,表示rehash操作完成
渐进式rehash的好处在于它采取分而治之的方式,将rehash键值对的计算均摊到每个字典的增删改查操作上,避免了集中式rehash的庞大计算量。
一次完整的渐进式rehash过程如下:
在此过程中,字典的增删改查操作会同时在ht[0],ht[1]两个表上进行,比如:
查找一个键,会先在ht[0]中查找,没找到再到ht[1]中找。
新添加到字典的键值对一律会被保存到ht[1]中而不是ht[0].
总结
(1)redis字典使用哈希表作为底层实现,哈希表使用链地址法解决键冲突
(2)每个字典有两个哈希表,ht[0]在平常使用,ht[1]在rehash时使用
(3)rehash并不是一次性完成的,而是渐进式的进行。