字典
字典,map,是用于保存键值对的抽象数据结构,是hash表实现。字典中的键唯一,通过键来操作值。Redis的数据库使用字典来作为底层实现。
定义
Redis的字典使用哈希表作为底层实现,一个哈希表里面由多个哈希表节点,哈希表节点保存着键值对。
哈希表
哈希表结构定义包含:哈希表数组,哈希表大小,哈希表掩码,哈希表已有节点数。
1 typedef struct dictht {
2 dicEntry **table;
3 unsigned long size;
4 unsigned long sizemask;
5 unsigned long used;
6 }dictht;
table就是哈希表数组,每个元素就是一个哈希表节点的指针。
size记录了哈希表的大小,也就是table的大小。
sizemask是哈希表的掩码,用于计算索引值,值总是size-1。
used就是哈希表已有节点数,注意与size进行区分哈希表大小并不等于节点数。
哈希表节点
哈希表节点结构包含:键,值,下一哈希表节点的指针(用于解决冲突)
1 typedef struct dictEntry {
2 void *key
3 union {
4 void *val
5 uint64_t u64;
6 int64_t s64;
7 } v;
8 struct dictEntry *next;
9 } dictEntry;
next指针将哈希值相同的键值对连接在一起,形成链表,解决冲突。
字典
字典包含:一个大小为2的哈希表数组(方便rehash),rehash索引,特定类型的函数,复制键的函数。
1 typedef struct dict {
2 dictType *type;
3 void *privdata;
4 dictht ht[2];
5 int trehashidx;
6 } dict;
type 和privdata的作用具体并不清楚,书上介绍“针对不同类型的键值对,为创建多态字典而设置”。
ht即hashtable平时的哈希表节点在ht[0],rehash的时候用到ht[1]。
哈希方式
插入键值对时,先根据键计算出哈希值。再根据哈希值和哈希表的掩码计算出应放在哈希表的哪个索引上。哈希函数为Murmurhash算法,然而并不知道具体是怎么个实现,给自己挖个坑,有空学习下。
hash = dict->type->hashFunction(key);
index = hash & dict->ht[x].sizemask;(x为0或1,取决于实际情况)
提到哈希肯定要考虑冲突的解决方法。在上面的哈希表节点中就已经看到,在这里使用链地址法解决冲突。
单向链表而且没有记录尾节点,所以插入链表时使用头插入。如果插入到末尾的话还需要遍历到链表末尾,消耗时间。
rehash
负载因子 = 哈希表已保存的节点数量/哈希表大小
很显然负载因子大说明哈希表太小,为了避免冲突就要增大。而负载因子太小说明哈希表过大,浪费了空间。
我们要对哈希表进行扩展或者收缩。这个工作通过rehash来完成。
- 为ht[1]分配空间。
- 将ht[0]的键值对rehash到ht[1]
- 当ht[0]全部迁移到ht[1],释放ht[0],让ht[1]成为新ht[0]
从上面的过程中我们发现有三个问题:
- 在什么情况下我们需要对哈希表进行扩展或收缩?
- 新空间分配策略是怎样的?
- rehash迁移的过程如何完成?
第一个问题:if 负载因子大于1且没有执行BGSAVE,BGREEWRITEAOF 或者 负载因子大于5 (具体为啥这么定我也不知道。。。第二个坑)
执行扩展
else if 负载因子小于0.1
执行收缩
第二个问题:扩展的大小为:大于等于(ht[0].used*2)的最小2的整数幂
缩小的大小为:大于等于(ht[0].used)的最小2的整数幂
第三个问题:渐进式rehash
rehash的过程为了不堵住服务,将分成几次完成。
- 为ht[1]分配空间,将rehashidx设为0(不进行rehash的时候值为-1)
- 每次对字典进行增删查改,程序将同时将rehashidx索引上的所有键值对rehash到ht[1] ,完成后rehashidx加一。
- 当ht[0]上的所有键值对全部完成rehash,将rehashidx设为-1。rehash完成
在rehash过程中,字典的删查改将在两个hashtable上进行,而增加操作只会在ht[1]进行。