Redis字典源码学习
特别提醒,本文所涉及的代码是redis3.0,由《Redis设计与实现》作者所注释的版本:地址
与字典相关的文件在
dict.h
和dict.c
两个文件中
数据结构
1 | typedef struct dictEntry { |
dictEntry
是字典中的单个节点,即一个k-v
对,注意到这里的值是一个union
联合体,键值对中的值可以是一个指向实际值的指针即val
,或者是一个无符号的 64 位整数u64
或有符号的 64 位整数s64
。
1 | typedef struct dictht { |
dictht
是一个哈希表,其中存放了很多个链表节点,每个链表是一个dictEntry
1 | typedef struct dict { |
dict
是redis
中真正提供给外面使用的字典,其中有多个字段,这里只关注核心的两个字段dictht ht[2]
和int rehashidx
。这里要创建两个哈希表是为了方便rehash
,rehashidx
主要用于渐进式哈希。
可能第一眼看上面有三个相关的数据结构,有点不太好理解,下面画一个图来描述三者的关系:
下面介绍学习一下dict
的一些增删改查的源码
查找
在字典dict
中查找key
在哪个链表中,如果存在该key
则返回-1
,不存在则返回哈希值,即应该在哪个链表中1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27static int _dictKeyIndex(dict *d, const void *key)
{
unsigned int h, idx, table;
dictEntry *he;
if (_dictExpandIfNeeded(d) == DICT_ERR)
return -1;
// 计算 key 的哈希值
h = dictHashKey(d, key);
// T = O(1)
for (table = 0; table <= 1; table++) {
// 计算索引值
idx = h & d->ht[table].sizemask;
// 查找 key 是否存在
// T = O(1)
he = d->ht[table].table[idx];
while(he) {
if (dictCompareKeys(d, key, he->key))
return -1;
he = he->next;
}
// 如果运行到这里时,说明 0 号哈希表中所有节点都不包含 key
// 如果这时 rehahs 正在进行,那么继续对 1 号哈希表进行 rehash
if (!dictIsRehashing(d)) break;
}
// 返回索引值
return idx;
}
- 首先调用函数
_dictExpandIfNeeded(d)
判断当前字典是否需要进行扩容 - 先遍历
ht[0]
中的链表,然后计算key
的哈希值,如果该链表中查询到则直接返回 - 如果在
ht[0]
的链表中没找到,则要先判断该字典是否处在rehash
中,如果不在,则直接返回前面计算到的idx
,否则还要去ht[1]
对应的链表中查找
1 | static int _dictExpandIfNeeded(dict *d) |
- 如果字典还未初始化,则进行初始化操作,初始化链表数组大小为4
- 满足两个条件之一,则需要进行扩容操作:字典已使用节点数和字典大小之间的比率接近
1:1
并且dict_can_resize
为真,或者已使用节点数和字典大小之间的比率超过dict_force_resize_ratio
,该值为5。 - 扩容时有个小细节,就是它调用
dictExpand(d, d->ht[0].used*2)
,传入的大小为已经存在的k-v
对的两倍
1 | int dictExpand(dict *d, unsigned long size) |
- 首先会根据传入的
size
大小,找到第一个大于等于size
的2
的幂次方 - 如果
ht[0]
为空,则说明这是一次初始化操作 - 如果非空,则说明这是一次
rehash
操作,需要打开rehash
标识,让字典在进行增删改查的时候进行渐进式哈希。
添加
往一个字典dict
中添加一个k-v
对,如果key
已经存在字典当中,则添加失败,否则插入该k-v
对
1 | int dictAdd(dict *d, void *key, void *val) |
- 首先调用
dictAddRaw
函数,判断字典d
中是否存在key
- 如果已经存在则直接返回错误
- 不存在,则插入,并且返回成功
1 | dictEntry *dictAddRaw(dict *d, void *key) |
- 首先判断字典是否处在
rehash
中,如果是,则顺带执行单步rehash
- 首先使用
_dictKeyIndex(d, key)
获取到插入到哪个链表的索引 - 如果当前正在
rehash
则应该插入到ht[1]
表中,否则是ht[0]
表中 - 插入到链表的头部(时间复杂度是O(1))
修改
如果键不存在,则将给定的键值对添加到字典中,如果键已经存在,那么删除旧有的键值对
1 | int dictReplace(dict *d, void *key, void *val) |
- 首先调用
dictAdd(d, key, val)
判断是否成功,如果成功说明是添加操作,否则要进行修改操作 - 调用
dictFind(d, key)
找到该key
对应的节点,修改其中的值
删除
查找并删除包含给定键的节点,成功删除返回DICT_OK
,没找到则返回DICT_ERR
1 | static int dictGenericDelete(dict *d, const void *key, int nofree) |
- 如果当前正在
rehash
,则协同进行单步的rehash
操作 - 首先遍历
ht[0]
,如果在该链表中没找到,则取遍历ht[1]
找到则直接删除