在上一篇文章中我们分析了redis中的字符串和双向链表的实现,这篇文章主要用来分析redis中的dict,数据结构设计的相当巧妙,代码写的相当精彩。
3.dict --- hash table implementation
redis被称为基于Key-Value内存数据库,其内部的最重要的数据结构就是字典(或哈希表),之所以能够高效率的完成CRUD,与dict的具体实现有密不可分的关系,这里我也是一起学习redis的dict的实现,很多都是自己的理解,有时候可能不够全面。
3.1 dict的数据结构
数据结构设计的好坏直接影响了编程实现的难易程度和dict的效率。redis的dict采用的是常用的数组加链表的形式来表示hash table,利用链表来解决哈希冲突问题。
首先我们来看一下哈希表的节点数据结构:dictEntry,定义如下:
1
2
3
4
5
6
7
8
9
|
typedef
struct
dictEntry {
void
*key;
//键
union
{
//值,union类型
void
*val;
uint64_t u64;
int64_t s64;
} v;
struct
dictEntry *next;
// 指向下个哈希表节点,形成链表
} dictEntry;
|
下面是一个哈希表的数据结构。
table是一个二级指针,指向的是一个数组,数组里面的元素全为指针,指针类型为dictEntry*,也就是说数组里面的每一个元素指向一个哈希节点。table其实就是一个指针数组;
size指的是数组大小,在这里也被称为哈希表大小,或者桶大小。
sizemask是哈希表的掩码,sizemask=size-1;这个是用来计算桶的索引值的,就是根据key,计算该key应该被映射到哪一个桶里面。在每一次申请dictht大小的时候,申请的大小都为2的指数幂。比如,我们申请16个大小的桶的时候,其二级制表示为10000,那么sizemask的大小为1111,也就是说sizemask是最大的桶的编号(从0号开始),那么当新来一个key是,我们只需要计算hash(key)&sizemaske,就可以得出,该key应该被映射到哪一个桶里面。平常我们计算桶的时候都是利用同余%来计算的,同余%的计算开销肯定要比位运算符&的开销大很多,在redis,这个操作时再频繁不过的了。当然有利于提高计算的性能。
used是用来记录该哈希表中已经有多少个哈希计算也就是dictEntry的数量了,用来统计桶中元素的,在判断时候应该rehash的时候用。(rehash指的就是由于dictEntry的数量增加或减少,当前的哈希表大小已经不能够达到快速增删改查的目的,那么我们就需要对重新建立一个hash表,然后对以前hash表里面的元素重新hash到新表里面去。比如,当used/size >5的时候,也就是说已有节点数是哈希表大小的5倍,也就是说每个桶里面平均至少有5个元素,已经严重影响了性能)
1
2
3
4
5
6
7
8
9
|
/*
* 哈希表
*/
typedef
struct
dictht {
dictEntry **table;
// 哈希表数组
unsigned
long
size;
// 哈希表大小(也就是桶大小,数组大小),指的是sizeof(*table)
unsigned
long
sizemask;
// 哈希表大小掩码,用于计算索引值
unsigned
long
used;
// 该哈希表已有节点的数量(指的是dictEntry的数量)
} dictht;
|
1
2
3
4
5
6
7
8
9
10
|
/*
* 字典
*/
typedef
struct
dict {
dictType *type;
// 类型特定函数
void
*privdata;
// 私有数据
dictht ht[2];
// 哈希表
long
rehashidx;
// rehash 索引,当 rehash 不在进行时,值为 -1
int
iterators;
// 目前正在运行的安全迭代器的数量
} dict;
|
1
2
3
4
5
6
7
8
9
10
11
|
/*
* 字典类型特定函数
*/
typedef
struct
dictType {
unsigned
int
(*hashFunction)(
const
void
*key);
//计算hash值
void
*(*keyDup)(
void
*privdata,
const
void
*key);
//复制key的值
void
*(*valDup)(
void
*privdata,
const
void
*obj);
//复制value的值
int
(*keyCompare)(
void
*privdata,
const
void
*key1,
const
void
*key2);
//两个键的比较函数
void
(*keyDestructor)(
void
*privdata,
void
*key);
//释放key(销毁key)
void
(*valDestructor)(
void
*privdata,
void
*obj);
//释放value( 销毁value)
} dictType;
|
privdata,字典的私有数据指针。
ht[2],在这字典申请了两个哈希表,目的很简单就是为了rehash,在redis中ht[0],是存放真正存放数据的哈希表,ht[1]是只有rehash的时候才会用到。那么对于一个字典dict来说,有两种状态:1.没有rehash。2.正在rehash(rehashing)。这样就需要一个成员来保存dict的状态信息,这样的话就引出了下一个rehashidx成员
rehashidx:当其值为-1时,表示的是不在rehash,而当其值大于等于0时,表示的增在进行rehash,而且当前已经rehash到了rehashidx所指向的这个桶中。
iterators:字典中安全迭代器的个数。
在redis中定了了用来遍历dict的迭代器,其定义如下:
1
2
3
4
5
6
7
8
9
10
11
|
typedef
struct
dictIterator {
dict *d;
//指向要迭代的字典
long
index;
//迭代器当前所指向的哈希表索引位置
//table:正在被迭代的hash表,dict中申请了两个hash表,值可以为0或1
//safe:表示这个迭代器是否安全
int
table, safe;
//entry:指向当前迭代到的节点指针
//nextEntry:指向下一个迭代节点的指针
dictEntry *entry, *nextEntry;
long
long
fingerprint;
//用于非安全迭代器计算字典指纹
} dictIterator;
|
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
27
28
29
30
31
|
long
long
dictFingerprint(dict *d) {
long
long
integers[6], hash = 0;
int
j;
integers[0] = (
long
) d->ht[0].table;
integers[1] = d->ht[0].size;
integers[2] = d->ht[0].used;
integers[3] = (
long
) d->ht[1].table;
integers[4] = d->ht[1].size;
integers[5] = d->ht[1].used;
/* We hash N integers by summing every successive integer with the integer
* hashing of the previous sum. Basically:
*
* Result = hash(hash(hash(int1)+int2)+int3) ...
*
* This way the same set of integers in a different order will (likely) hash
* to a different number. */
for
(j = 0; j < 6; j++) {
hash += integers[j];
/* For the hashing step we use Tomas Wang's 64 bit integer hash. */
hash = (~hash) + (hash << 21);
// hash = (hash << 21) - hash - 1;
hash = hash ^ (hash >> 24);
hash = (hash + (hash << 3)) + (hash << 8);
// hash * 265
hash = hash ^ (hash >> 14);
hash = (hash + (hash << 2)) + (hash << 4);
// hash * 21
hash = hash ^ (hash >> 28);
hash = hash + (hash << 31);
}
return
hash;
}
|
3.2 字典的创建,初始化,以及常用操作
在这里我们主要通过字典中所提供的API之间的调用关系来一窥其内部的实现机制。
3.2.1 字典的创建
首先我们来看一下,字典的创建:字典创建开始于:dictCreate---->_dictInit---->_dictReset。其具体的代码如下所示:
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
27
28
|
/* Create a new hash table */
dict *dictCreate(dictType *type,
void
*privDataPtr)
{
dict *d = zmalloc(
sizeof
(*d));
//申请空间
_dictInit(d,type,privDataPtr);
//对其中的元素进行初始化
return
d;
}
/* Initialize the hash table */
int
_dictInit(dict *d, dictType *type,
void
*privDataPtr)
{
_dictReset(&d->ht[0]);
//初始化0号哈希表
_dictReset(&d->ht[1]);
//初始化1号哈希表
d->type = type;
d->privdata = privDataPtr;
d->rehashidx = -1;
d->iterators = 0;
return
DICT_OK;
}
static
void
_dictReset(dictht *ht)
{
ht->table = NULL; //
并没有为table申请空间
ht->size = 0;
ht->sizemask = 0;
ht->used = 0;
}
|
3.2.2 添加元素到字典中
往字典中
第一次添加元素的调用过程为:dictAdd---->dictRaw---->_dictKeyIndex---->_dictExpandIfNeeded---->dictExpand。这一过程是前面的函数调用其后面的函数,具有层级关系。下面我们来从最低层的dictExpand开始分析。
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
|
/* 扩展或者创建字典 */
int
dictExpand(dict *d, unsigned
long
size)
{
dictht n;
// 创建一个新的哈希表
unsigned
long
realsize = _dictNextPower(size);
//计算最小的大于size的2的幂次方的值
if
(dictIsRehashing(d) || d->ht[0].used > size)
//如果这个字典正在rehash,或者要创建的字典大小比使用的节点数要小的话,不能扩展
return
DICT_ERR;
/* 从新分配内存,注意在这里n.table进行了初始化 */
n.size = realsize;
n.sizemask = realsize-1;
n.table = zcalloc(realsize*
sizeof
(dictEntry*));
n.used = 0;
/* 判断字典是否为第一次初始化,如果是,就不需要扩展了,直接将申请的哈希表赋值给0号哈希表就好了. */
if
(d->ht[0].table == NULL) {
d->ht[0] = n;
return
DICT_OK;
}
/* 到这里了,表明字典是需要扩展的,那么就将新申请的哈希表赋值给1号哈希表,并设置rehashidx为0,表明rehash 0号桶 */
d->ht[1] = n;
d->rehashidx = 0;
return
DICT_OK;
}
|
1)如果字典是第一次初始化,直接将申请的哈希表赋值给字典中的0号哈希表。
2)如果字典是需要扩展的,那么就将新的哈希表赋值给1号哈希表,并设置rehashidx,表明正在rehash
思路很清晰,dictExpand不仅仅可以用来初始化,同样可以用来扩展字典。(其实从函数命名上来看,其实应该说,它可以用来对字典扩展的同时,也提供了字典初始化的工作,这里初始化仅仅是哈希表)
3)dictExpand仅仅是申请了一个空间给1号哈希表,并没有将0号哈希表里面的值hash到这个1号哈希表中,仅仅是设置状态rehashidx,表明字典正在进行rehash操作,有必要再强调这一点。
下面我们来看一下什么条件下可以进行dictExpand操作:_dictExpandIfNeeded。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
/* 根据需要对字典扩展 */
static
int
_dictExpandIfNeeded(dict *d)
{
if
(dictIsRehashing(d))
return
DICT_OK;
//rehash已经在进行了
if
(d->ht[0].size == 0)
return
dictExpand(d, DICT_HT_INITIAL_SIZE);
//如果0号哈希表的大小为0,按初始化大小进行扩展
/* 条件:
* 1.字典已使用节点数大于字典大小,也可以说起比率接近1:1
* 2.字典可以被rehash或者字典中使用节点数和字典大小之间的比率超过dict_force_resize_ratio
* 只有上述两个条件同时满足的时候才会对字典扩展,扩展的大小至少为现在已使用节点的两倍 */
if
(d->ht[0].used >= d->ht[0].size &&
(dict_can_resize ||
d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
{
return
dictExpand(d, d->ht[0].used*2);
}
return
DICT_OK;
}
|
1)如果字典中的0号哈希表还没有初始化,我们就执行dictExpand(d, DICT_HT_INITIAL_SIZE),其中
DICT_HT_INITIAL_SIZE的值为4,在dict.h头文件里面定义也就是说初始化字典的大小为4
2)如果一下条件同时满足,也可以对字典扩展,扩展的大小至少为现在已使用节点的两倍
条件1.字典中字典已使用节点数与字典大小的比率接近1:1
条件2.字典可以被rehash(指的是dict_can_resize)或者字典中使用节点数和字典大小之间的比率超过dict_force_resize_ratio。其中dict_can_resize和dict_force_resize_ratio定义在dict.c中,如下所示
static int dict_can_resize = 1; // 指示字典是否启用 rehash 的标识
static unsigned int dict_force_resize_ratio = 5;// 强制 rehash 的比率
也就是说可以通过dict_can_resize来表示字典可以进行rehash了,或者通过
dict_force_resize_ratio 来对字典进行强制rehash。
下面我们看一下_dictKeyIndex函数:
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
27
28
29
|
/**
* 函数主要用来根据指定的key,
* 返回该key应该放在字典的哪一个桶里面
* 如果返回key已经存在,返回-1
**/
static
int
_dictKeyIndex(dict *d,
const
void
*key)
{
unsigned
int
h, idx, table;
dictEntry *he;
/* Expand the hash table if needed */
if
(_dictExpandIfNeeded(d) == DICT_ERR)
return
-1;
/* Compute the key hash value */
h = dictHashKey(d, key);
/* 如果增在rehash的话,需要返回1号哈希表的索引值 */
for
(table = 0; table <= 1; table++) {
idx = h & d->ht[table].sizemask;
/* Search if this slot does not already contain the given key */
he = d->ht[table].table[idx];
while
(he) {
if
(dictCompareKeys(d, key, he->key))
return
-1;
he = he->next;
}
if
(!dictIsRehashing(d))
break
;
}
return
idx;
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
dictEntry *dictAddRaw(dict *d,
void
*key)
{
int
index;
dictEntry *entry;
dictht *ht;
if
(dictIsRehashing(d)) _dictRehashStep(d);//单步rehash操作
if
((index = _dictKeyIndex(d, key)) == -1)
return
NULL;
/*如果正在rehash,说明index指向的是1号哈希表,否则指向的是0号*/
ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
entry = zmalloc(
sizeof
(*entry));
entry->next = ht->table[index];
ht->table[index] = entry;
ht->used++;
dictSetKey(d, entry, key);
return
entry;
}
|
最外层的添加寒素是dictAdd函数,其内容很简单,就是直接调用dictAddRaw来返回的插入的节点的指针,如果key不在dict中,添加该键值对,返回添加成功,否则返回添加失败
,表明该key已经存在,不能添加。其代码如下:
1
2
3
4
5
6
7
8
|
int
dictAdd(dict *d,
void
*key,
void
*val)
{
dictEntry *entry = dictAddRaw(d,key);
if
(!entry)
return
DICT_ERR;
dictSetVal(d, entry, val);
return
DICT_OK;
}
|
3.2.3 替换给定的键值对(也就是update操作)
我们经常在一些拥有字典数据结构语言中看到诸如 a[key]=value 这样的赋值表达式,通常我们的解释是,如果字典中纯在key的话,就将其对应的值更新为value。如果不存在的话,就新添加一个key/value节点。同样,redis也提供了这样的操作的函数:dictReplace。代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
/*
* 用于增加一个哈希节点,如果字典中已经存在相应的key,
* 就用val更新相应的值
**/
int
dictReplace(dict *d,
void
*key,
void
*val)
{
dictEntry *entry, auxentry;
/* Try to add the element. If the key
* does not exists dictAdd will suceed. */
if
(dictAdd(d, key, val) == DICT_OK)
return
1;
/* It already exists, get the entry */
entry = dictFind(d, key);
auxentry = *entry;
dictSetVal(d, entry, val);
dictFreeVal(d, &auxentry);
return
0;
}
|
3.2.4 删除操作(remove)
redis的删除操作分为两种,删除和nofree删除。删除操作的时候,删除哈希节点的同时也删除key和value指向值。nofree仅仅删除节点而不删除key和value所指向的值。删除操作函数如下:
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
27
28
29
30
31
32
33
34
35
36
37
|
/* Search and remove an element */
static
int
dictGenericDelete(dict *d,
const
void
*key,
int
nofree)
{
unsigned
int
h, idx;
dictEntry *he, *prevHe;
int
table;
if
(d->ht[0].size == 0)
return
DICT_ERR;
/* d->ht[0].table is NULL */
if
(dictIsRehashing(d)) _dictRehashStep(d); //单步rehash
h = dictHashKey(d, key);
for
(table = 0; table <= 1; table++) {
idx = h & d->ht[table].sizemask;
he = d->ht[table].table[idx];
prevHe = NULL;
while
(he) {
if
(dictCompareKeys(d, key, he->key)) {
/* Unlink the element from the list */
if
(prevHe)
prevHe->next = he->next;
else
d->ht[table].table[idx] = he->next;
if
(!nofree) {
dictFreeKey(d, he);
dictFreeVal(d, he);
}
zfree(he);
d->ht[table].used--;
return
DICT_OK;
}
prevHe = he;
he = he->next;
}
if
(!dictIsRehashing(d))
break
;
}
return
DICT_ERR;
/* not found */
}
|
操作中,用的最多的是for循环,用于遍历0号哈希表和1号哈希表。while循环,用来遍历桶里面的元素。一般桶里面的元素是非常少的,从_dictExpandIfNeeded中可以看出,当平均一个桶里面的元素达到5个的时候就会执行强制rehash操作。而大部分时候都会在接近于1:1的情况下也会进行rehash,所以,一次查找,删除,增加,更改节点的操作时可以在很短的时间内完成的。
还有一点我们在介绍的过程中也看到了,每次操作都进行了一次单步渐进式rehash操作。那到底什么是渐近式rehash呢?它能给我们带来什么呢?这会在下一篇文章中详细介绍