1.Redis的字典是什么?
Redis字典,又叫符号表/关联数组/映射,是一种用于保存键值对的抽象数据结构。还可以作为Redis数据库的底层,哈希键的底层等。
2.如何实现Redis字典
自顶向下来实现一个Redis字典,使用Redis的实现语言-C语言
1)字典
typedef struct dict{
dicType *type;//类型特定函数
void *privdata;//私有数据
dictht ht[2];//哈希表
int trehashidx;//rehash索引,当rehash不再进行时,值为-1
}
- type属性和privdata属性是针对不同类型的键值对,为创建多态字典而设置的:
– privdata属性:保存了需要传给那些特定类型函数(见下)的可选参数
– type属性:指向一个dictType结构的指针,每个该结构都保存了一簇用于操作特定类型的键值对的函数,Redis自动为不同的字典类型设置类型特定函数`
typedef struct dictType{
unsigned int (*hashFunction)(const void *key);//计算哈希值的函数
void *(*keyDup)(void*privdata,const void *key);//复制键的函数
void *(*valueDup)(void*privdata,const void *obj);//复制值的函数
int (*keyCompare)(void *privadata,const void *key1,const void *key2);//对比值的函数
int (*keyDestructor)(void *privdata,void *key);//销毁键的函数
int (*valDestructor)(void *privdata,void *obj);//销毁值的函数
- ht属性是包含了两个项的数组,每个项都是一个dictht哈希表(详情见下)。一般情况,字典只使用ht[0]哈希表,ht[1]哈希表只会在对ht[0]进行rehash的时候使用。
- rehashidx属性也是和rehash有关的,记录了当前rehash的进度,默认是-1。
上图是普通状态下的字典,箭头表示将表中的小项进行展开,本节也是按照这种结构撰写的
2)哈希表
Redis字典的底层数据结构包括哈希表结构
typedef struct dictht{
dictEntry **table;//哈希表数组
unsigned long size;//哈希表大小
unsigned long sizemask;//哈希表大小掩码,用于计算索引值。sizemask = size - 1;
unsigned long used;//该哈希表已有的节点的数量
}
哈希表数组table的每个元素指向一个dictEntry结构,也就是哈希表节点
3)哈希表节点
字典以哈希表作为底层实现,一个哈希表包括多个哈希节点,一个哈希节点保存一个字典中的键值对。
typedef struct dictEntry{
void *key;//键
/*值*/
union{//三种类型的数据
void *val;
unit64_t u64;
int64_t s64;
}v;//值的名字,有以上三种选择
struct dictEntry *next;//指向下一个哈希节点,形成链表,解决哈希冲突问题,关于哈希冲突将在下边进行介绍
}
3.哈希算法
1)什么是哈希算法
- 在学习HashMap的时候我们对hash算法有了一定的了解.。 在Redis中,当一个新的键值添加到字典中时,程序会根据键值对的键计算出哈希值和索引值,根据索引值将包含新键值对的哈希表节点放入 哈希表数组 的指定索引上。
- 键的哈希值使用哈希函数
hash=dict->type->hashFunction(key)
计算,之后通过哈希表的sizemask属性值和哈希值,使用公式index=hash&dict->ht[x].sizemask
计算出索引值,其中ht[x]中的x可能是0或1。 - 关于MurmurHash算法可以看这个网站:算法主页
2)如何解决哈希冲突
- 哈希冲突:当两个及以上的键被分配到哈希表数组的同一个索引上面时,这些键发生哈希冲突。
- Redis哈希表使用链地址法解决键冲突:前边讲哈希表节点的结构时讲过,哈希表节点有一个next指针指向下一个哈希节点,多个哈希表节点可以用指针构成一个单项链表,针对分配到同一个索引上的多个节点可以奖它们用链表连接,这样就有效解决了哈希冲突问题。
- 一些使用中的惯例:新添加的节点要添加到链表的表头位置,因为dictEntry节点没有指向表尾的指针。
3)rehash
- 背景:随着不断的操作,哈希表保存的键值对也在增加/减少,为了维持哈希表的负载因子在合理的范围内,需要对哈希表的大小进行管理:键值对数量过多就扩展,数量过少就收缩。
- 以上工作都是通过rehash(重新散列)操作完成,rehash的步骤如下:
– 1、为字典的ht[1]分配空间:取决于要执行和操作、ht[0]当前包含键值对的数量。
如果要扩展哈希表,ht[1]的大小是2的n次方,且恰好大于等于ht[0].used*2的那个
如果是收缩哈希表,ht[1]的大小是2的n次方,且恰好大于等于ht[0].used的那个
– 2、将ht[0]上的所有键值对rehash到ht[1]上,rehash就是重新计算键的哈希值和索引值,之后将键值对放在新的哈希表的指定位置上
– 3、ht[0]的所有键值对迁移到ht[1]之后,释放空表ht[0],将ht[1]设置为ht[0],设置一个空白的哈希表为新的ht[1](为下一次rehash做准备) - 什么时候会发生哈希表的扩展?
– 1、服务器当前没有执行BGSAVE命令/BGREWRITEAOF命令,并且哈希表的负载因子>=1
– 2、服务器当前正在执行BGSAVE命令/BGREWRITEAOF命令,并且哈希表的负载因子>=5
负载因子的计算公式如下:
//负载因子 = 哈希表已保存的节点数量 / 哈希表大小
load_factor = ht[0].used / ht[0].size;
- 什么时候会发生哈希表的收缩?:负载因子 < 0.1时。
4) 渐进式rehash
- 什么时候使用渐进式rehash?
当ht[0]的键值对数量不多的时候,可以一次性rehash到ht[1]中。当ht[0]保存的键值对数量到了百万级别这种很大的规模的时候,一次性的rehash到ht[1]中计算量太大可能导致计算机崩溃,这时需要分多次、渐进的将ht[0]的键值对rehash到ht[1]中。 - 怎么执行渐进式rehash操作?
1、先根据ht[0]的键值对数量判断是否要执行渐进式rehash,之后为ht[1]分配空间、
2、在字典中维持一个索引计数器变量rehashidx = 0
,表示rehash工作开始
3、在rehash期间,对字典执行增删改查操作,还会将ht[0]在rehashidx索引上的所有键值对rehash到ht[1]上,rehash完成时rehashidx++
4、当所有的键值对都被rehash到ht[1]上是,rehashidx = -1表示rehash操作结束
分治思想在rehash中得到了使用,将操作均摊到每个添加、删除、查找、更新操作上,避免集中式rehash带来的巨大计算量