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,定义如下:
由于是利用链表来解决问题的,所以其实就是一个单链表的形式,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个元素,已经严重影响了性能)
这样的一个哈希表已经记录了足够多的信息,而在redis中由于要考虑rehash的问题,所以最终的字典结构如下:
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的迭代器,其定义如下:
在定义中我们可以看到有个safe成员,他用来标识是否为安全迭代器,安全迭代器在进行迭代的时候是有可能会对当前entry进行修改的,所以需要一个nextEntry来保存下一个迭代节点的位置,防止后面的不会被迭代到。而fingerprint是用来保存非安全迭代器的指纹的,这样在迭代器迭代过程中就可以根据fingerprint的值来比较迭代的过程中是否有数据发生过变化。fingerprint的计算如下所示:
主要是利用dict的中的6个元素的特征值,进行一次hash操作来进行计算。
3.2字典的创建,初始化,以及常用操作
在这里我们主要通过字典中所提供的API之间的调用关系来一窥其内部的实现机制。
3.2.1字典的创建
首先我们来看一下,字典的创建:字典创建开始于:dictCreate---->_dictInit---->_dictReset。其具体的代码如下所示:
dictCreate是用申请空间用的,_dictInit是初始化各种字典值,注意,在初始化哈希表的时候,_dictReset并没有为table申请空间,仅仅是将其赋值为NULL。可以想象,table的初始化,肯定是发生在第一次往字典中添加元素的时候进行。
3.2.2添加元素到字典中
往字典中第一次添加元素的调用过程为:dictAdd---->dictRaw---->_dictKeyIndex---->_dictExpandIfNeeded---->dictExpand。这一过程是前面的函数调用其后面的函数,具有层级关系。下面我们来从最低层的dictExpand开始分析。
dictExpand的作用用来创建一个新的哈希表:
1)如果字典是第一次初始化,直接将申请的哈希表赋值给字典中的0号哈希表。
2)如果字典是需要扩展的,那么就将新的哈希表赋值给1号哈希表,并设置rehashidx,表明正在rehash
思路很清晰,dictExpand不仅仅可以用来初始化,同样可以用来扩展字典。(其实从函数命名上来看,其实应该说,它可以用来对字典扩展的同时,也提供了字典初始化的工作,这里初始化仅仅是哈希表)
3)dictExpand仅仅是申请了一个空间给1号哈希表,并没有将0号哈希表里面的值hash到这个1号哈希表中,仅仅是设置状态rehashidx,表明字典正在进行rehash操作,有必要再强调这一点。
下面我们来看一下什么条件下可以进行dictExpand操作:_dictExpandIfNeeded。
从代码中可以看出:
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函数:
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; |
下面是一个哈希表的数据结构。
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; //私有数据 dicththt[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; /*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; } |
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; } |
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; } |
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
最外层的添加寒素是dictAdd函数,其内容很简单,就是直接调用dictAddRaw来返回的插入的节点的指针,如果key不在dict中,添加该键值对,返回添加成功,否则返回添加失败,表明该key已经存在,不能添加。其代码如下:
3.2.3替换给定的键值对(也就是update操作) 我们经常在一些拥有字典数据结构语言中看到诸如a[key]=value这样的赋值表达式,通常我们的解释是,如果字典中纯在key的话,就将其对应的值更新为value。如果不存在的话,就新添加一个key/value节点。同样,redis也提供了这样的操作的函数:dictReplace。代码如下
3.2.4删除操作(remove) redis的删除操作分为两种,删除和nofree删除。删除操作的时候,删除哈希节点的同时也删除key和value指向值。nofree仅仅删除节点而不删除key和value所指向的值。删除操作函数如下:
|