您的位置:首页 > 理论基础 > 数据结构算法

Redis 3.0 源码解析---底层数据结构分析(2)

2014-11-18 22:37 756 查看
在上一篇文章中我们分析了redis中的字符串和双向链表的实现,这篇文章主要用来分析redis中的dict,数据结构设计的相当巧妙,代码写的相当精彩。

3.dict---hashtableimplementation
redis被称为基于Key-Value内存数据库,其内部的最重要的数据结构就是字典(或哈希表),之所以能够高效率的完成CRUD,与dict的具体实现有密不可分的关系,这里我也是一起学习redis的dict的实现,很多都是自己的理解,有时候可能不够全面。
3.1dict的数据结构
数据结构设计的好坏直接影响了编程实现的难易程度和dict的效率。redis的dict采用的是常用的数组加链表的形式来表示hashtable,利用链表来解决哈希冲突问题。
首先我们来看一下哈希表的节点数据结构:dictEntry,定义如下:

1

2

3

4

5

6

7

8

9
typedef
struct
dictEntry{


void
*key;
//键


union
{
//值,union类型


void
*val;


uint64_tu64;


int64_ts64;


}v;


struct
dictEntry*next;
//指向下个哈希表节点,形成链表


}dictEntry;


由于是利用链表来解决问题的,所以其实就是一个单链表的形式,key是键,值可以为void*类型或者uint64_t,int64_t类型。void*的key也就是说键可以是任意类型的,void*的value表明值也可以是任意类型的。
下面是一个哈希表的数据结构。
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;


这样的一个哈希表已经记录了足够多的信息,而在redis中由于要考虑rehash的问题,所以最终的字典结构如下:

1

2

3

4

5

6

7

8

9

10
/*


*字典


*/


typedef
struct
dict{


dictType*type;
//类型特定函数


void
*privdata;
//私有数据


dicththt[2];
//哈希表


long
rehashidx;
//rehash索引,当rehash不在进行时,值为-1


int
iterators;
//目前正在运行的安全迭代器的数量


}dict;


dictType是针对特定的字典定义的一系列特定操作,其具体定义如下

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;


总共有6个函数,每个函数复制特定的功能,基本都是针对dict中dictEntry的操作。还记得前面我们说过C语言的多态吗,这也是一种表现形式,你操作一个类型的dictEntry(指的是void*指向的数据类型),就需要定义相应的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;


在定义中我们可以看到有个safe成员,他用来标识是否为安全迭代器,安全迭代器在进行迭代的时候是有可能会对当前entry进行修改的,所以需要一个nextEntry来保存下一个迭代节点的位置,防止后面的不会被迭代到。而fingerprint是用来保存非安全迭代器的指纹的,这样在迭代器迭代过程中就可以根据fingerprint的值来比较迭代的过程中是否有数据发生过变化。fingerprint的计算如下所示:

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;


/*WehashNintegersbysummingeverysuccessiveintegerwiththeinteger


*hashingoftheprevioussum.Basically:


*


*Result=hash(hash(hash(int1)+int2)+int3)...


*


*Thiswaythesamesetofintegersinadifferentorderwill(likely)hash


*toadifferentnumber.*/


for
(j=0;j<6;j++){


hash+=integers[j];


/*ForthehashingstepweuseTomasWang's64bitintegerhash.*/


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;


}


主要是利用dict的中的6个元素的特征值,进行一次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
/*Createanewhashtable*/


dict*dictCreate(dictType*type,


void
*privDataPtr)


{


dict*d=zmalloc(
sizeof
(*d));
//申请空间


_dictInit(d,type,privDataPtr);
//对其中的元素进行初始化


return
d;


}


/*Initializethehashtable*/


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;


}


dictCreate是用申请空间用的,_dictInit是初始化各种字典值,注意,在初始化哈希表的时候,_dictReset并没有为table申请空间,仅仅是将其赋值为NULL。可以想象,table的初始化,肯定是发生在第一次往字典中添加元素的时候进行。
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)


{


dicthtn;
//创建一个新的哈希表


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,表明rehash0号桶*/


d->ht[1]=n;


d->rehashidx=0;


return
DICT_OK;


}


dictExpand的作用用来创建一个新的哈希表:
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中,如下所示
staticintdict_can_resize=1;//
指示字典是否启用rehash的标识
staticunsignedintdict_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;


/*Expandthehashtableifneeded*/


if
(_dictExpandIfNeeded(d)==DICT_ERR)


return
-1;


/*Computethekeyhashvalue*/


h=dictHashKey(d,key);


/*如果增在rehash的话,需要返回1号哈希表的索引值*/


for
(table=0;table<=1;table++){


idx=h&d->ht.sizemask;

/*Searchifthisslotdoesnotalreadycontainthegivenkey*/


he=d->ht
.table[idx];

while
(he){


if
(dictCompareKeys(d,key,he->key))


return
-1;


he=he->next;


}


if
(!dictIsRehashing(d))
break
;


}


return
idx;


}


该函数的作用作用主要是用来根据给定的key,去判断这个key应该被放在字典的哪一个桶里面。如果该key已经在字典中存在的话,那么就返回-1。从源码的for循环中可以看出,如果字典正在rehash,那么返回的idx是指向的1号哈希表的。

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;


}


dictAddRaw,就是调用_dictKeyIndex来获取add的哈希表中的桶的。如果已经key存在了,那么返回NULL,否则创建该节点并返回该节点的指针,在创建节点的时候需要判断该节点是否增在rehash,来决定将新建节点插入到哪个哈希表中。在这里dictAddRaw会执行一次单步rehash操作(后面我们会介绍redis的渐进式rehash策略)。
最外层的添加寒素是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;


}


从以上分析中可以看出,dictAdd,在往字典中添加元素的时候,所经历的操作。字典对添加元素进行了统一的处理,只是在第一次添加元素的时候做了特别的判断,之后再往字典中添加元素,走的同样是这个流程。_dictExpandIfNeeded就是用来扩展用的,也就是说每一次添加元素,我们都会判断是否执行这个操作。
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;


/*Trytoaddtheelement.Ifthekey


*doesnotexistsdictAddwillsuceed.*/


if
(dictAdd(d,key,val)==DICT_OK)


return
1;


/*Italreadyexists,gettheentry*/


entry=dictFind(d,key);


auxentry=*entry;


dictSetVal(d,entry,val);


dictFreeVal(d,&auxentry);


return
0;


}


代码很简单,先利用dictAdd来添加,如果添加失败,说明key已经存在,在利用dictFind来查找对应哈希节点。dictFind的查找过程很简单,找到返回对应节点的指针,否则返回NULL,和_dictKeyIndex有点想像,就是没有进行_dictExpandIfNeeded操作。但是同样find会进行一次单步rehash操作。(后面我们会介绍redis的渐进式rehash策略)
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
/*Searchandremoveanelement*/


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].tableisNULL*/


if
(dictIsRehashing(d))_dictRehashStep(d);//单步rehash


h=dictHashKey(d,key);


for
(table=0;table<=1;table++){


idx=h&d->ht.sizemask;

he=d->ht
.table[idx];

prevHe=NULL;


while
(he){


if
(dictCompareKeys(d,key,he->key)){


/*Unlinktheelementfromthelist*/


if
(prevHe)


prevHe->next=he->next;


else


d->ht
.table[idx]=he->next;



if
(!nofree){


dictFreeKey(d,he);


dictFreeVal(d,he);


}


zfree(he);


d->ht
.used--;

return
DICT_OK;


}


prevHe=he;


he=he->next;


}


if
(!dictIsRehashing(d))
break
;


}


return
DICT_ERR;
/*notfound*/


}


同样,在删除的时候也做了一次单步rehash操作。
操作中,用的最多的是for循环,用于遍历0号哈希表和1号哈希表。while循环,用来遍历桶里面的元素。一般桶里面的元素是非常少的,从_dictExpandIfNeeded中可以看出,当平均一个桶里面的元素达到5个的时候就会执行强制rehash操作。而大部分时候都会在接近于1:1的情况下也会进行rehash,所以,一次查找,删除,增加,更改节点的操作时可以在很短的时间内完成的。
还有一点我们在介绍的过程中也看到了,每次操作都进行了一次单步渐进式rehash操作。那到底什么是渐近式rehash呢?它能给我们带来什么呢?这会在下一篇文章中详细介绍
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息