redis數據結構---dict

        最近剛剛看過了redis源碼的字典(dict)部分,結合《redis設計與實現》一書中的關於字典數據結構的介紹,寫下自己對於redis中使用的字典數據結構的理解。如果有不正確的地方,歡迎各位及時指正。

        dict是redis中關鍵的一種數據結構,數據庫和哈希鍵的實現都依賴於dict結構。根據看過的資料,個人認爲要掌握redis中的dict數據結構需要掌握以下幾方面。

        1.redis中字典的實現所用的數據結構以及他們彼此之間的關係;

        2.redis中rehash的實現;

        3.redis中dict的遍歷操作dictScan的實現;

        本文重點關注第1、2部分,關於dictScan、的精妙設計思想。請參考博客,講解的通俗易懂;另一篇博客中的圖配合的比較好。自認爲不能比這兩位博主寫出更好的關於dictScan的理解,所以在建議讀者直接去閱讀這兩篇文章。

        下面,按照以上列出的幾個問題,介紹redis的實現和源碼。

1、redis中字典的實現所用的數據結構以及他們彼此之間的關係

        redis的實現中包含了struct dict、struct dictht、struct dictEntry、struct dictType以及struct dictIterator五種數據結構,其代碼實現分別如下:

  typedef struct dict {
      //每個dictype結構保存了一簇用於操作特定類型鍵值對的函數,redis爲用途不同的字典設置不同類型的特定函數;
      dictType *type;
      //保存用於傳遞給特定類型函數的可選參數;
      void *privdata;
      dictht ht[2];
      long rehashidx; /* rehashing not in progress if rehashidx == -1 */
      //???iterators是什麼???
      unsigned long iterators; /* number of iterators currently running */
  } dict;

        type 和 privatedata 屬性是針對不同類型的的鍵值對,爲創建多態字典而存在的。type是一個指向dictType類型的指針,每個dictType保存了一簇用戶操作特定類型的鍵值對的函數(有針對key的哈希函數、key的銷燬函數、val的銷燬函數、key的比較函數、key的複製函數和val的複製函數),redis會爲不同類型的鍵值對設置不同類型的特定函數;privatedata屬性則保存了需要傳遞給那些特定類型函數的可選參數。

        ht[2]中包含了兩個dictht結構。每個dictht都是一個哈希表,其中ht[0]一般是默認的哈希表,ht[1]只有在redis執行rehash的時候纔會使用。

        rehashidx是redis執行 rehash 的過程中,保存的當前需要執行的rehash在ht[1]中的索引。在不執行rehash的時候,rehashidx爲-1,dict.h中判斷dict是否處於rehash的宏定義就是使用了rehashidx是否爲-1。(#define dictIsRehashing(d) ((d)->rehashidx == -1)。

        iterator中代表是否是處於安全的迭代過程中。

  /*哈希表結構,每個字典中含有兩個這樣的哈希表結構,
   * 存在兩個的原因是因爲我們爲了控制哈希表的負載因子在合理的範圍內時,
   * 可能需要rehash,rehash採用的"漸進式哈希"*/
  /* This is our hash table structure. Every dictionary has two of this as we
   * implement incremental rehashing, for the old to the new table. */
  typedef struct dictht {
      //指向指針的指針,代碼的實現中table指向了包含有size個dictEntry*結構的數組的首地址;
      dictEntry **table;
      //數組的總的大小;
      unsigned long size;
      //sizemask和哈希值一起決定一個節點應該放到哈希表的哪個索引上面;
      unsigned long sizemask;
      //數組中已有的元素的大小;因爲使用了鏈式衝突分解方案,因此有可能used的值大於size的值;
      unsigned long used;
  } dictht;

     sizemask 的大小爲 size - 1,其主要作用是針對給定的key的哈希值,確定該key在table中的索引(idx = (d->type->hashfunciton(key)) & sizemask)以保證key所對應的節點的索引不會溢出而超過size-1。

        used爲dictht中已經存儲的元素的個數。根據這個used/size的值以及負載因子的大小,確定是否應該執行rehash;根據used元素的值確定rehash的時候新的dictht的大小爲第一個大於或者等於ht[0].used * 2大小的2的n次方。

  typedef struct dictEntry {
      void *key;
      union {
          void *val;
          uint64_t u64;
          int64_t s64;
          double d;
      } v;
      struct dictEntry *next;
  } dictEntry;

        從next可以看出,redis使用了鏈式衝突分解方案解決鍵衝突,哈希值的低log2(ht[0].size)位相同的鍵在同一張哈希表上具有相同的索引。當索引產生衝突的時候,將新增的鍵值對增加到已有索引的鏈表的頭部,這樣可以減少遍歷該索引上整個鏈表的不必要的麻煩,以O(1)的時間複雜度進行插入。

        key 是一個指針,源碼中使用Murmurhash()方法計算出該key對應的哈希值,然後與dictht中的sizemask做"位與",保證索引的值始終處於哈希表的 0 ~ size - 1範圍之內。

  typedef struct dictType {
      uint64_t (*hashFunction)(const void *key);
      void *(*keyDup)(void *privdata, const void *key);
      void *(*valDup)(void *privdata, const void *obj);
      int (*keyCompare)(void *privdata, const void *key1, const void *key2);
      void (*keyDestructor)(void *privdata, void *key);
      void (*valDestructor)(void *privdata, void *obj);
  } dictType;

        hashFunction 爲對key進行哈希所使用的哈希函數,redis中使用了Murmurhash()函數。該hash算法具有較好的分佈均勻性;

        keyDup 爲複製鍵key的函數,如果是key是一個對象,則可以是該對象的複製構造函數;

        valDup 爲複製值的函數,如果val是一個對象,那麼可以是該值的複製構造函數;

        keyCompare 爲對比兩個鍵值是否相等的函數;

        keyDesstructor和valDestructor分別爲key和val的析構函數,如果key和val是動態分配的對象的話,那麼這兩個函數將是對應的釋放函數。這在刪除一個dictEntry的時候是非常有用的,例如dict.h文件中定義了兩個宏,分別用於釋放key和val。

  #define dictFreeKey(d, entry) \
      if ((d)->type->keyDestructor) \
          (d)->type->keyDestructor((d)->privdata, (entry)->key)
  #define dictFreeVal(d, entry) \
      if ((d)->type->valDestructor) \
          (d)->type->valDestructor((d)->privdata, (entry)->v.val)
  typedef struct dictIterator {
      dict *d;
      long index;
      /** safe = 0 表示是一種非安全的迭代器;safe = 1 表示是一種安全的迭代器 **/
      int table, safe;
      dictEntry *entry, *nextEntry;
      /* unsafe iterator fingerprint for misuse detection. */
      long long fingerprint;
  } dictIterator;

        redis的迭代器分爲安全的迭代器和非安全的迭代器。所謂的安全迭代器就是在執行的過程中可以執行增、刪、查等操作;但是非安全的迭代器只能執行迭代操作,迭代的過程中通過dictFingerPrint驗證是否對dict進行了修改。

        d 爲指向迭代的dict的指針,這個在dictNext中獲取指定的dict;

        table爲dict中ht的下標,取值爲0或者1,index爲需要遍歷的ht中成員table的下標;

        safe代表是否是安全的迭代;

        entry爲當前迭代器指向的entry,nextEntry爲當前entry的下一個entry。之所以保留nextEntry是因爲當前的entry極有可能被調用者刪除,因此使用nextEntry記錄下一個entry,供下次迭代使用。

        fingerprint 相當於指紋,在進行不安全的迭代的時候,首先記錄該dict的指紋(關於ht[0]和ht[1]的size和used的一些散列計算,等迭代結束的時候再次驗證指紋,如果前後的指紋一致,表明沒有對dict做什麼插入和刪除操作)。

        dictIterator主要用在對某個字典進行迭代的過程中。

        上面敘述了幾種組成redis中的dict的重要數據結構,下面我們給出他們之間的組成關係圖。


圖 1 dict組成結構圖
       這裏需要說明的一點是,dictht是直接包含在dict中的,每個dict包含兩個dictht,之所以這麼畫,是爲了能夠區分出各個結構體,並理清他們之間的關係。

2、redis中rehash的實現

        當初始化一個dict之後,隨着操作的進行,dict中dictht的size以及used都將不斷地變化。如果不及時調整dict中dictht的大小,那麼隨着其中元素的增加,將造成每個索引的鏈表長度變得很長,查找速度變慢;隨着元素的減少,可能存在很多的空閒的slot,造成了內存空間的浪費。爲了最優化redis的性能,redis可以動態調整dict中ht[0]的大小,這個過程稱之爲rehash。總之,隨着redis的使用,redis可以動態擴容或者縮容dict。

        在redis中的rehash是通過使用空表ht[1]完成的。當redis沒有執行BGSAVE和BGREWRITEAOF,且負載因子大於等於1時,會執行rehash或者是當redis中正在執行BGSAVE或者BGREWRITEAOF,但是負載因子大於等於5時會執行rehash(負載因子 = ht[0].used/ht[0].size),且redis爲了避免在執行rehash的過程會過長的阻塞進程從而無法接受其他的請求,redis對dict的rehash操作採用了“分而治之”的策略---漸進式哈希,在redis中執行增、刪、改、查的時候將在當前rehashidx上的鏈表rehash到ht[1]。這個過程的基本單位是一個index上的整個鏈表中的元素, 因此不存在一個index上的部分dictEntry被rehash,而其他部分的dictEntry沒有被rehash的情況。除了在每次的增、刪、查的過程中執行rehash,redis在databaseCron中同樣會執行rehash。  

        在增、刪、改、查的過程中rehash的執行都是通過_dictRehashStep(dict* d)在執行的,其定義如下。

  /** 只執行一步的哈希 **/
  static void _dictRehashStep(dict *d) {
      if (d->iterators == 0) dictRehash(d,1);
  }
        _dictRehashStep表明是要單步執行rehash,rehash執行的就是d->rehashidx索引上的整條鏈表。真正的執行操作是在dictRehash中進行的,其定義如下。
  int dictRehash(dict *d, int n) {
      /** 防止過多的遍歷ht中相應的index位置鏈表爲NULL的slots,對遍歷空白index的數量做一個限制 **/
      int empty_visits = n*10;
      if (!dictIsRehashing(d)) return 0;
  
      /** ht[0].used != 0 表示當前肯定沒有rehash完;while中的n-- 和 d->ht[0].used != 0既保證了rehash的index的數量不會超過n,也保證了rehash的dictEntry數量不會超過當前d->ht[0]中已有的數量 **/
      while(n-- && d->ht[0].used != 0) {
          dictEntry *de, *nextde;
  
          assert(d->ht[0].size > (unsigned long)d->rehashidx);
          /** 跳過 hash table 中指針爲空的槽 **/
          while(d->ht[0].table[d->rehashidx] == NULL) {
              /** 此處不用擔心d->rehashidx會在已有的尺寸中溢出,因爲該循環只是跳過指針爲空的槽,ht[0].used已經保證了還沒有哈希完 **/
              d->rehashidx++;
               /** 已經遍歷的slot的數量達到了空slot的數量,返回 1 表示還沒有rehash完 **/
              if (--empty_visits == 0) return 1;
          }
          de = d->ht[0].table[d->rehashidx];
           /* 將該索引上鍊表上的所有dictEntry轉移到ht[1]中 */
          while(de) {
              unsigned int h;
              
              nextde = de->next;
              /* 獲取de->key的哈希值,並與新的hash table中的sizemask做位與獲取新的索引值;這裏需要注意的一點的是,key不變,一般hash值也不會發生
  變化,但是由於sizemask發生了變化也會造成最終的索引值發生變化。關於這一點可以參考上面兩片博客中關於dictScan的介紹 */
  
              /** 在進行rehash的過程中,並不會釋放掉舊的dictEntry再重新分配dictEntry結構體,只是改變指向該dictEntry的的指針而已 **/
              h = dictHashKey(d, de->key) & d->ht[1].sizemask;
              /** 爲了節省插入時間,在鏈表頭進行插入操作 **/
              de->next = d->ht[1].table[h];
              d->ht[1].table[h] = de;
              d->ht[0].used--;
              d->ht[1].used++;
              de = nextde;
          }
           /** rehash 完一個索引上的dictEntry之後,將舊ht中索引爲rehashidx的位置指向NULL **/
          d->ht[0].table[d->rehashidx] = NULL;
          /** 遞增d->rehashidx爲下次rehash做準備 **/
          d->rehashidx++;
      }   
          
      /* Check if we already rehashed the whole table... */
      /** 結束再哈希 **/
      if (d->ht[0].used == 0) {
          /** 只有 hash table 中的table是動態分配的,所以必須手動釋放掉 **/
          zfree(d->ht[0].table);
          d->ht[0] = d->ht[1];
          _dictReset(&d->ht[1]);
          d->rehashidx = -1;
          return 0;
      }
  
      /* More to rehash... */
      return 1;
  }

        前面我們說redis採用了漸進式rehash的方式,將整個rehash的過程分攤到每次對dict的增、刪、改、查中,在redis的源碼中每次執行dictEntry *dictAddRow(dict *d,void *key,dictEntry **existing),static dictEntry *dictGenericDelete(dict *d, const void *key,int nofree),dictEntry *dictFind(dict *d, const void *key),dictEntry *dictGetRandomKey(dict *d),unsigned int dictGetSomeKeys(dict *d, dictEntry **des,unsigned int count)的時候如果當前處於rehash的狀態,那麼就要執行dictRehashStep(dict *d)操作。

        在執行rehash的過程中如果對dict執行增加操作,那麼新增加的元素將全部增加到redis中的ht[1]表中,而不是默認的ht[0]表,這就保證了新增加的元素只會增加到ht[1]中,而ht[0]中的元素只會減少不會增加。那麼在某一個時刻,ht[0]表中的元素將會全部轉移到ht[1]中,完成rehash的過程。下面我們看一下,向dict中增加元素的時候,redis是如何做的。

  int dictAdd(dict *d, void *key, void *val)
  {
      dictEntry *entry = dictAddRaw(d,key,NULL);
  
      if (!entry) return DICT_ERR;
      /** 將值添加到 entry 中 **/
      /** 將鍵和值與entry的結合分爲兩個不同的地方,看似不符合《代碼大全》的要求,但是方便與對於set實現。set的實現中只需要使用dictAddRaw,沒有val **/
      dictSetVal(d, entry, val);
      return DICT_OK;
  }
 /** dictEntry ** existing 如果不爲null, 則當key已經存在的時候,existing是指向已經存在的 dictEntry* 的指針,如果existing爲null, 那麼即使key已經存在,也不指向已經存在的dictEntry* ,此時該函數的返回值是null;
   * 如果 key不存在,那麼直接返回新添加的dictEntry指針 **/
  /** 反映在命令行上就是,如果key已經存在,那麼直接返回0,如果key不存在那麼返回1; **/
  dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
  {
      int index;
      dictEntry *entry;
      dictht *ht;
  
      /** rehash過程是一個漸進式哈希的過程,將整個再哈希過程分攤到每次對redis字典的增、刪、改、查操作中 **/
      if (dictIsRehashing(d)) _dictRehashStep(d);
  
       /* 獲取要新增加的元素的key對應的index,如果不能成功獲取key對應的index,那麼返回 -1,表示key已經存在或者是需要擴展hash table操作沒有成功 */
      if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)
          return NULL;
          
      /** 如果正在處於哈希的過程中,那麼直接取dict中的ht[1]而不是默認的ht[0]。這就符合上面所說的,如果實在哈希的過程中,那麼增加元素的時候將直>  接在ht[1]中增加元素,ht[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++;
      
      /** 將 key 與新分配額entry相結合 **/
      dictSetKey(d, entry, key);
      return entry; 
  }   
        既然,在rehash的過程中,將新增的元素只會添加到ht[1]哈希表中,那麼當rehash時在獲取哈希表中entry所在的索引時必須獲取ht[1]中的索引,而不是ht[0]中的索引。下面還是看redis中的代碼實現。
  static int _dictKeyIndex(dict *d, const void *key, unsigned int hash, dictEntry **existing)
  {
      unsigned int idx, table;
      dictEntry *he;
      if (existing) *existing = NULL;
  
       /* 如果滿足要求,則擴展redis中的hash table */
      if (_dictExpandIfNeeded(d) == DICT_ERR)
          return -1;
      /** 對整個dict中的兩個hash table進行遍歷,如果key存在於任何一個hash table中,那麼證明key不能被再次添加 **/
      for (table = 0; table <= 1; table++) {
          /* 獲取redis中ht[table]中key所在的索引 */
          idx = hash & d->ht[table].sizemask;
          /* key是否已經存在 */
          he = d->ht[table].table[idx];
          while(he) {
              if (key==he->key || dictCompareKeys(d, key, he->key)) {
                  if (existing) *existing = he;
                  return -1;
              }   
              he = he->next;
          }   
          /* 如果dict沒有正在處於rehash中,那麼不必要再次遍歷dict中的ht[1],因爲此時它爲空 */
          if (!dictIsRehashing(d)) break;
      }   
      return idx;
  }   
  static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) {
      unsigned int h, idx;
      dictEntry *he, *prevHe;
      int table;
  
      if (d->ht[0].used == 0 && d->ht[1].used == 0) return NULL;
  
      /* 漸進式rehash過程,每次執行增、刪、改、查都要執行單步的rehash */
      if (dictIsRehashing(d)) _dictRehashStep(d);
      h = dictHashKey(d, key);
  
      /* 遍歷兩個hash table */
      for (table = 0; table <= 1; table++) {
          idx = h & d->ht[table].sizemask;
          he = d->ht[table].table[idx];
          prevHe = NULL;
          while(he) {
              if (key==he->key || dictCompareKeys(d, key, he->key)) {
                  /* Unlink the element from the list */
                  if (prevHe)
                      prevHe->next = he->next;
                  else
                      /** 處理找到的 key 位於鏈表頭的情況,此時preHe爲NULL **/
                      d->ht[table].table[idx] = he->next;
                  /** 判斷是否是直接釋放掉找到的dictEntry所佔用的空間,如果是則直接釋放掉dictEntry所佔用的內存空間 **/
                  if (!nofree) {
                      dictFreeKey(d, he);
                      dictFreeVal(d, he);
                      zfree(he);
                  }   
                  d->ht[table].used--;
                  return he;
              }
              prevHe = he;
              he = he->next;
          }
          /** 哈希表沒有處在rehash過程中,ht[1]爲空表 */
          if (!dictIsRehashing(d)) break;
      }
      return NULL; /* not found */
  }
        在查找一個給定的鍵的時候,也是在dict的兩個表中進行遍歷,當然了,如果是處於非再哈希的過程中,那麼直接在ht[0]中遍歷就可以了。上代碼。
  dictEntry *dictFind(dict *d, const void *key)
  {
      dictEntry *he;
      unsigned int h, idx, table;
  
      if (d->ht[0].used + d->ht[1].used == 0) return NULL; /* dict is empty */
      if (dictIsRehashing(d)) _dictRehashStep(d);
      h = dictHashKey(d, key);
      for (table = 0; table <= 1; table++) {
          idx = h & d->ht[table].sizemask;
          he = d->ht[table].table[idx];
          while(he) {
              if (key==he->key || dictCompareKeys(d, key, he->key))
                  return he;
              he = he->next;
          }
          if (!dictIsRehashing(d)) return NULL;
      }
      return NULL;
  }

        當然,dictGetSomeKeys和dictGetRandomKey兩個函數執行的時候,如果dict處於rehash的過程中,那麼都是在兩個hash table中執行查找過程。

        關於redis中dict的介紹,本文到此就結束了。但是還是非常建議讀者去閱讀開頭提到的兩篇博客中關於dictScan中使用的算法和執行rehash時redis對於兩個hash table的遍歷方法,實在是非常的巧妙。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章