前言
Redis的五大数据类型之一的hash
,当一个哈希键存储的键值对满足一定条件时,Redis会转用字典进行键值对的存储。
字典
一个普通状态下的字典,其结构会是这样的:
Redis中的字典的结构如下:
typedef struct dict{
//类型特定函数
dictType *type;
//私有数据
void *privdata;
//哈希表
dictht ht[2];
//rehash索引;当rehash不在进行时,值为-1
int rehashidx;
}dict;
type
属性和privdata
属性是针对不同类型的键值对,为了创建多态字典而设置的:
type
属性是一个指向dictType
结构的指针,每个dictType
结构保存了一簇用于操作特定类型键值对的函数,Redis会为用途不同的字典设置不同的类型特定函数privdata
属性则保存了需要传给那些类型特定函数的可选参数
dictht
之所以是一个长度为2的数组,这种设计与后面要提及的rehash
操作有关。
dictType结构
typedef struct dictType{
//计算哈希值的函数
unsigned int (*hashFunction)(const void *key);
//复制键的函数
void *(*keyDup)(void *privdata,const void *key);
//复制值的函数
void *(*valDup)(void *privdata,const void *obj);
//对比键的函数
int (*keyCompare)(void *privdata,const void *key1,const void *key2);
//销毁键的函数
void (*keyDestructor)(void *privdata,void *key);
//销毁值的函数
void (*valDestructor)(void *privdata,void *obj1);
} dictType;
dictht结构
ht属性是一个包含两个项的数组,数组中的每个元素都是一个dictht
哈希表
而这个dictht
结构如下:
typedef struct dictht{
//哈希表数组
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,用于计算索引值,总是等于size-1
unsigned long used;
} dictht;
table属性是一个数组,其内每一个元素都是一个dictEntry
结构的指针。每个指针结构都保存着一个键值对。
dictEntry结构
typedef struct dictEntry{
//键
void *key;
//值
union{
void *val;
uint64_tu64;
int64_ts64;
} v;
//指向下个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
- key属性:保存着键值对中的键
- v属性:保存着键值对中的值,这个值可以是一个指针、或是一个uint64_t整数或者是一个int64_t整数
- next属性:一个指向另一个哈希表节点的指针
示例
加入有这么两个键k1和k0,其索引值都是相同的,那么他们在dictht
中会是这样存放的:
哈希算法
在这个dictEntry
数组中,当添加一个新的键值对到字典里面时,大致步骤:
- 程序根据键值对的键计算哈希值和索引值
- 根据索引值,将新的键值对放入到
dictEntry
数组中的指定位置上 - 当发生哈希冲突的时候,Redis使用链地址法来解决。并且采用的是头插法,即冲突时插入的新节点会成为表头节点
值得一提的是,因为dictEntry
组成的链表没有指向链表表尾的指针,所以为了速度考虑,Redis才采用头插法,将新节点添加到链表的表头位置。
示意图如下:
rehash
随着操作的不断进行,哈希表保存的键值对会逐渐变多或者变少,为了让哈希表的负载因子维持在一个合理的范围内,rehash
是必不可少的。rehash
即为哈希表的大小进行相应的扩展或者收缩。
扩展和收缩哈希表的工作可以通过执行rehash(重新散列)操作来完成
步骤如下:
- 为字典的
ht[1]
哈希表分配空间,这个空间的大小取决于要进行的操作是扩展还是收缩- 如果执行的是扩展操作,那么
ht[1]
的大小将会为第一个大于或等于ht[0].used*2
的2n。例如ht[0].used*2=14
,那么接下来的大小就应该为16(24) - 如果执行的是收缩操作,那么
ht[1]
的大小为第一个大于或等于ht[0]*used
的2n的值
- 如果执行的是扩展操作,那么
- 将保存在
ht[0]
中的键值对rehash
到ht[1]
上面:rehash
指的是重新计算键的哈希值和索引值,然后将键值对放置到ht[1]
哈希表的指定位置上 - 当键值对都迁移完成后,将
ht[0]
释放,然后将ht[1]
设置为ht[0]
,并为ht[1]
新创建一个空白哈希表,为下一次rehash
做准备
示例
接下来要队字典进行扩展操作,示意图如下:
步骤如下:
(1)计算ht[1]
该分配的大小,这里为4*2=8(23)。所以ht[1]
的大小将会为8
(2)将ht[0]
包含的键值对都rehash
到ht[1]
(3)释放ht[0]
,将ht[1]
设置为ht[0]
,重新为ht[1]
分配一个空白哈希表
渐进式rehash
如果字典中存储了四百万、四千万个甚至四亿个键值对时,一次性将这些键值对全部rehash
到ht[1]
的话,可能会导致服务器在一段时间内停止服务。
因此为了避免这种情况,rehash是分多次、渐进式地将ht[0]
里面的键值对慢慢的rehash
到ht[1]
去
步骤:
- 为
ht[1]
分配空间,让字典同时持有两个哈希表:ht[0],ht[1]
- 将字典中的
rehashidx
属性设置为0,表示rehash
工作正式开始 - 在
rehash
进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行对应的操作外,还会顺带将ht[0]
哈希表在rehashidx
索引上的所有键值对rehash
到ht[1]
,当rehash
工作完成后,将rehashidx
值增一 - 随着字典操作的不断执行,最终在某个时间点上,
ht[0]
的所有键值对都会被rehash
到ht[1]
,这时程序将rehashidx
重新设置为-1,表示rehash
操作完成
值得一提的是,在rehash
过程中,字典会同时使用ht[0]
和ht[1]
两个哈希表,所以在渐进式rehash
过程中,字典的 删查改 等操作会在两个哈希表上进行。例如:找一个键时,在ht[0]
中找不到,就会去ht[1]
中找。
当新增一个键值对时,会将其保存到ht[1]
中,而ht[0]
不会再进行任何操作。这种措施保证了ht[0]
里的键值对只减不增,并随着rehash
操作而最终变成空表。