1.总结:
1. 字典被用于实现redis的功能包括数据库和哈希键
2.Redis 字典的底层实现采用哈希表,每个字典采用两个哈希表 一个平时用,一个在rehash的时候用
3. Redis使用MurmurHash2的算法来进行hash计算
4. Redis对于产生了hash冲突的键采用链地址法结局hash冲突,并且是头插法形成一个单向链表
5.在对hash表进行扩展和收缩的时候 需要采用渐进式hash的方法进行扩展或收缩
2. 分解
2.1 字典的数据结构 (dict.h/dict)
typedef struct
{
dictType *type;// 类型特定的函数
void * privatedata; //私有数据
dictht *[2];//哈希表
int trehashidx;//索引值在rehash的时候作为标记使用
}
//dictType的数据结构
typedef struct{
unsigned int(*hashfunction)(const void*key);//计算hash值的函数
void*(*keyDup)(void*privatedata, void*key);//复制键的函数
.
.
.
}
2.2 hash 表的数据结构 (dict.h/dictht)
typedef struct dictht
{
dictEntry **table; //hash表数组 存储hash之后的键值对
unsigned long size;//哈希表的大小
unsigned long sizemask;//哈希表的掩码,用于与hash函数计算hash的键
unsigned long used;//已经使用的节点数量
}
//hash表数据节点的结构提
typedef struct dictEntry{
void *key;//数据的键
union{
void *val;
uint64_t u64;
int64_t i64
};//联合的结构体存储不同的数据
struct dictEntry*next;//指向下一个节点 用于解决hash冲突
}
2.3 整体结构图
3.知识要点
3.1 如何解决hash冲突
redis是使用的MurmurHash2算法来计算hash值的 当需要在字典里面插入一个值的时候 调 dict->type->hashFunction(key) 来计算得到一个hash值,然后用得到的hash值与dict->ht[x].sizemask进行&操作 得到具体需要插入的索引值,如果由多个键得到的hash结果是一样的,那么在产生hash冲突的时候,redis采用的是链地址法来进行处理 也就是用dictEntry->next 来保存这个产生的hash 冲突的键值对 并且是插入到前面,之所以插入到前面是一般新创建的键 使用的频率比较高 这样方便查找
3.2 渐进式rehash(hash的扩容和收缩操作)
- 1.为ht[1]分配空间 分配空间的规则是 如果是扩容操作就是第一个2^n的值大于等于ht[0]已经使用空间的两倍 即 2^n >= ht[0].used*2 如果是收缩操作就是第一个2^n的值大于等于ht[0]已经使用空间 即2^n >= ht[0].used
- 2.在字典ht[0]中维持一个索引计数器rehashidx 并在开始的时候将它置为0,表示rehash正式的开始工作
-
- 在rehash 期间程序的添加,删除 查找和更新的操作,程序除了执行指定的操作 还将ht[0]中rehashidx所指向的键值对rehash到ht[1],完成之后rehashidx的值加1 ps 在rehash的过程中 所有新添加的键一律都放入到ht[1]中 这样就可以保证ht[0]中的数据 只减少不增加
- 4.在将ht[0]中的所有数据都rehash到ht[1]的时候 将ht[1]修改为ht[0]并且将rehashidx置为-1
**采用渐进式rehash的方法 可以有效的避免数据区扩展或收缩的时候 统一处理所带来庞大的计算量,采用分而治之的思想 **
3.3 字典的api
- dictcreate 创建一个新的字典
- dictadd 添加一个新的键值对
- dictReplace 将给定的键值对添加到字典里面 如果已经存在就直接替换
- dictFetchValue 返回给定键的值
- dictGetRandomKey 从字典中随机返回一个键值对