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

        在上一篇文章中我们分析了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;
       由于是利用链表来解决问题的 ,所以其实就是一个单链表的形式,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;     // 私有数据
     dictht ht[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;
 
     /* 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;
}
主要是利用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
/* 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;
}
    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)
{
     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;
}
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中,如下所示
                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;
}
      该函数的作用作用主要是用来根据给定的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;
 
     /* 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;
}
代码很简单,先利用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
/* 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 */
}
同样,在删除的时候也做了一次单步rehash操作。
       操作中,用的最多的是for循环,用于遍历0号哈希表和1号哈希表。while循环,用来遍历桶里面的元素。一般桶里面的元素是非常少的,从_dictExpandIfNeeded中可以看出,当平均一个桶里面的元素达到5个的时候就会执行强制rehash操作。而大部分时候都会在接近于1:1的情况下也会进行rehash,所以,一次查找,删除,增加,更改节点的操作时可以在很短的时间内完成的。
        还有一点我们在介绍的过程中也看到了,每次操作都进行了一次单步渐进式rehash操作。那到底什么是渐近式rehash呢?它能给我们带来什么呢?这会在下一篇文章中详细介绍


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值