字典的实现
Redis字典所使用的哈希表由dict.h/dictht结构定义:1
2
3
4
5
6
7
8 HLEN website
(integer) 10086
HGETALL website
1)"Redis"
2)"Redis.io"
3)"MariaDB"
...
1 | typedef struct dictht { |
哈希表节点使用dictEntry结构表示,每个dictEntry结构都保存着一个键值对1
2
3
4
5
6
7
8
9
10
11
12typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 指向下个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
- key属性保存着键值对中的键,
- 而v属性则保存着键值对中的值,其中键值对的值可以是一个指针,或者是一个unit64_t证书,又或者是一个int64_t整数。
- next指向另一个哈希表节点,可以将多个哈希值相同的键值对连接在一起。
Redis中的字典由dict.h/dict结构表示:1
2
3
4
5
6
7
8
9
10
11typedef struct dict {
// 类型特定函数
dictType *type;
// 私有数据
void *privdata;
// 哈希表
dictht ht[2];
// rehash索引
// 当rehash不在进行时,值为-1
int trehashidx;
} dict
- type属性是一个指向dictType结构的指针,每个dictType结构保存了一簇用于操作特定类型键值对的函数,Redis会为用途不同的字典设置不同的类型特定函数。
- private属性保存了需要传给那些类型特定函数的可选参数。
- ht属性是一个包含两个项的数组,数组中的每个项都是一个dictht哈希表,一般情况下,字典只使用ht[0]哈希表,ht[1]哈希表只会在对ht[0]哈希表进行rehash时使用。
- rehashidx记录了rehash的进度,-1表示没有进行rehash。
哈希算法
Redis计算哈希值和索引值的方法如下:1
2
3
4// 计算键key的哈希值
hash = dict->type->hashFunction(key);
// 计算出索引值
index = hash & dict->ht[x].sizemask;
当字典被用作数据库的底层实现,或者哈希键的底层实现时,Redis使用MurmurHash2算法来计算键的哈希值。
解决键冲突
Redis使用链地址法,程序总是将新节点添加到链表的表头位置(这点和Java中的HashMap也很像)。
rehash
步骤如下:
- 为字典的ht[1]哈希表分配空间,如果是扩展操作,ht[1]大小为第一个大于等于ht[0].used*2的2^n;如果是收缩操作,ht[1]大小为第一个大于等于ht[0].used的2^n。
- 将保存在ht[0]中的所有键值对rehash到ht[1]上面
- 释放ht[0],将ht[1]设置为ht[0],并在ht[1]新创建一个空白哈希表。
哈希表的扩展与收缩
满足下列任意一个条件时,自动开始扩展操作
- 服务器目前没有在执行BGSAVE或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于1。
- 服务器目前正在执行BGSAVE或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于5。
负载因子load_factor = ht[0].used / ht[0].size
另一方面,当哈希表的负载因子小于0.1时,自动执行收缩操作。
渐进式rehash
为了避免rehash对服务器性能造成影响,当服务器里键值对过多时,详细步骤如下
- 为ht[1]分配空间
- 维持一个索引计数器变量rehashidx,并设为0,表示rehash工作正式开始
- 在rehash进行期间,每次对字典进行增删改查操作时,会将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1],rehash完成后,rehashidx++;
- 随着字典不断执行,最终ht[0]的所有键值对都会被rehash到ht[1],这时设置rehashidx为-1,表示rehash操作已完成。
渐进式rehash执行期间的哈希表操作
- 删除 查找 更新会在两个哈希表上进行
- 添加操作一律在ht[1]执行