redis數據結構---zskiplist

        複習《redis設計與實現》,跳躍表是在看《redsi設計與實現》的時候第一次接觸到的概念,因此認真研究了一下跳躍表的性質以及redis源碼中針對跳躍表的實現。下面對redis源碼中關於跳躍表的實現配合源碼做詳解的解釋(註釋都包含在源碼中)。至於跳躍表的性質及查找的複雜度(平均爲O(log(N))、最差爲O(N)),自行google,相關文章非常之多。本篇文章只記錄當時自己花費時間比較多去理解的部分。

 1、跳躍表的數據結構

        在redis中實現的跳躍表使用了跳躍表zskiplist和跳躍表節點在zskiplistNode兩種struct。下面分別進行解釋。(在redis 4.0.1版本中zskiplistNode和zskiplist定義在server.h中,並非redis3.0中的redis.h中,如果讀者閱讀源碼,查找)。

        ·zskiplistNode結構體

  typedef struct zskiplistNode {
      /* redis3.0版本中使用robj類型表示,但是在redis4.0.1中直接使用sds類型表示 */
      sds ele;
      double score;
      struct zskiplistNode *backward;
      /** 這裏該成員是一種柔性數組,只是起到了佔位符的作用,在sizeof(struct zskiplistNode)的時候根本就不佔空間,這和sdshdr結構的定義是類似的(sds.  h文件); 如果想要分配一個struct zskiplistNode大小的空間,那麼應該的分配的大小爲sizeof(struct zskiplistNode) + sizeof(struct zskiplistLevel) *   count)。其中count爲柔性數組中的元素的數量
       **/
      struct zskiplistLevel {
          /* 對應level的下一個節點 */
          struct zskiplistNode *forward;
          /* 從當前節點到下一個節點的跨度 */
          unsigned int span;
      } level[];
  } zskiplistNode;

        ·zskiplist結構體

  typedef struct zskiplist {
      /* 跳躍表的頭結點和尾節點,尾節點的存在主要是爲了能夠快速定位出當前跳躍表的最後一個節點,實現反向遍歷 */
      struct zskiplistNode *header, *tail;
      /* 當前跳躍表的長度,保留這個字段的主要目的是可以再O(1)時間內獲取跳躍表的長度 */
      unsigned long length; 
      /* 跳躍表的節點中level最大的節點的level保存在該成員變量中。但是不包括頭結點,頭結點的level永遠都是最大的值---ZSKIPLIST_MAXLEVEL = 32。level的值隨着跳躍表中節點的插入和刪除隨時動態調整 */     
      int level;
  } zskiplist;

2、跳躍表中增、刪、查的實現

        ·刪除整個跳躍表,並釋放分配的空間

  void zslFree(zskiplist *zsl) {
  
      /** 根據跳躍表的性質,跳躍表的最低一層也就是level[0]的一層包含了跳躍表中的所有節點。因此,只需要依次釋放掉level[0]中forward指針所連接的節
  點即可 **/
      zskiplistNode *node = zsl->header->level[0].forward, *next;
  
      zfree(zsl->header);
      while(node) {
          next = node->level[0].forward;
          zslFreeNode(node);
          node = next;
      }
      /** 釋放掉zskiplist結構體 **/
      zfree(zsl);
  }

         ·向跳躍表中插入節點

  /** 將新的節點插入到zskiplist中。在調用該函數之前,由調用者確保該成員在zskiplist中不存在 **/
  /** 該程序不容易理解的話,可以按照redis中定義的zskiplist結構,將各種情況走一遍 **/
  zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
      /* update數組保存了要插入的新節點在插入之後各個level的緊鄰的前邊的節點 */
      zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
      /* rank數組保存在新節點插入之後所在位置的各個level的前面緊鄰節點在整個跳躍表中的排位,用於更新各個level的前面緊鄰節點的跨度和新節點到後面
  緊鄰節點的跨度 */
      unsigned int rank[ZSKIPLIST_MAXLEVEL];
      int i, level;
  
      /** 如果!isnan(score)爲0,那麼將記錄相關的錯誤信息,exit(1)退出程序 **/
      serverAssert(!isnan(score));
      x = zsl->header;
  
      /** 下面的for循環是爲了確定新的zskiplistNode節點應該插入的位置的前面的節點 x **/
      /** 按照在跳躍表中查找給定節點的順序,應該是按照forward和down的順序進行查找。首先是從最高層開始向後進行查找,如果當前節點的forward指針指向
  的節點的score小於給定的score或者說forward指向的節點的score等於給定的score但是forward指向的節點的ele的字典序小於給定的ele,應該繼續向後進行查>  找,否則應該向下(down)進行查找,對應於redis中的實現,zskiplist實現中,應該將層級減 1。在跳躍表中,在較高的層出現的zskiplistNode在較低的層肯定
  會出現;反之卻不一定 **/
      /** 注意這裏的 i 是從zsl->level-1開始的,而不是從最大的ZSKIPLIST_MAXLEVEL開始的 **/
      for (i = zsl->level-1; i >= 0; i--) {
          /* store rank that is crossed to reach the insert position */
          /** 初始化每一層的rank。如果是最高的一層,則初始化爲0;如果不是最大的level,則證明是由上一層級down下來的,應該初始化爲上一層的rank **/
          rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
          /** x代表在第 i 層要插入的節點的backward指針指向的節點;每層或者都不一樣或者也有可能是相同的 **/
          while (x->level[i].forward &&
                  (x->level[i].forward->score < score ||
                      (x->level[i].forward->score == score &&
                      sdscmp(x->level[i].forward->ele,ele) < 0)))
          {
              rank[i] += x->level[i].span;
              x = x->level[i].forward;
          }
          /** update保存了新插入的節點位置在第 i 層的前向節點 **/
          update[i] = x;
      }
      level = zslRandomLevel();
      /** 如果隨機產生的新的層級大於當前的zsl中保存的level(當前zsl的最大層級),那麼新的層級的backward節點全部指向zsl->header;此時當然在update>  中保存的也是update[i](i > zsl)應該是zsl->header; **/
      if (level > zsl->level) {
          for (i = zsl->level; i < level; i++) {
              rank[i] = 0;
              update[i] = zsl->header;
              /** 只是暫時的一個初始化,後面會進行相應的修改 **/
              update[i]->level[i].span = zsl->length;
          }
          /** 新插入的zskiplistNode的層大於現有的zsl->level,更改現有的zsk->level **/
          zsl->level = level;
      }
  
      x = zslCreateNode(level,score,ele);
      for (i = 0; i < level; i++) {
          /* 更新新插入的節點在每層的forward節點指向的節點 */
          x->level[i].forward = update[i]->level[i].forward;
          /* 更新新插入的節點所在位置前面緊鄰節點的forward指向新插入的節點 */
          update[i]->level[i].forward = x;
  
          /* 改變由於插入新的zskiplistNode導致的跨度的變化 */
          /** rank[0] 代表了要插入的 zskiplistNode 在實際的鏈表中的排位,rank[i]代表了在相應的層級的排位 **/
          /** 跨度如果不好明白的話,可以畫圖理解 **/
          x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
          update[i]->level[i].span = (rank[0] - rank[i]) + 1;
      }
  
      /* 如果新插入的節點的level小於原來的zsl->level,那麼那些大於新插入節點level的level並沒有指針指向新插入的節點,此時只需簡單的將其跨度增加   1 即可 */
      for (i = level; i < zsl->level; i++) {
          update[i]->level[i].span++;
      }
  
      /** header 節點的forward節點的backward節點不再指向header,而是指向NULL **/
      x->backward = (update[0] == zsl->header) ? NULL : update[0];
      /** 考慮到將新的zskiplistNode節點添加到最後一個節點的情況 **/
      if (x->level[0].forward)
          x->level[0].forward->backward = x;
      else
          zsl->tail = x;
      zsl->length++;
      return x;
  }

        個人認爲,跳躍表的整個難點就在於插入操作中。明白了插入操作的代碼做執行的每一步操作,那麼再去理解查找和刪除的代碼操作就很容易了。其中插入操作的關鍵點在於確定要新插入的節點所在的位置,所說的這種位置包括來那個方面,第一是新插入的節點在各個level的前面緊鄰接點;第二,新插入的節點的各個level的前面緊鄰節點在整個跳躍表中的rank。就是上面代碼中加粗的部分,如果不理解,根據《redis設計與實現》中的圖畫圖進行理解。這裏不再進行畫圖的操作。

        ·在跳躍表中查找節點

  zskiplistNode *zslFirstInRange(zskiplist *zsl, zrangespec *range) {
      zskiplistNode *x;
      int i;
  
      /* If everything is out of range, return early. */
      if (!zslIsInRange(zsl,range)) return NULL;
  
      x = zsl->header;
      for (i = zsl->level-1; i >= 0; i--) {
          /* Go forward while *OUT* of range. */
          /** 找到第一個大於(或者等於,具體取決於range->minex是否爲1)range->min的節點的前向節點並記錄下來 **/
          while (x->level[i].forward &&
              !zslValueGteMin(x->level[i].forward->score,range))
                  x = x->level[i].forward;
      }
  
      /* This is an inner range, so the next node cannot be NULL. */
      /* 根據條件而言,x 不可能爲NULL */
      x = x->level[0].forward;
      serverAssert(x != NULL);
  
      /* Check if score <= max. */
      if (!zslValueLteMax(x->score,range)) return NULL;
      return x;
  }

        其他在跳躍表中查找給定範圍條件的函數,zslLastInRange()、zslFirstInLexRange()和zslLastInlexRange()都和上面的zslFirstInRange()原理相同,在此不再進行贅述。

        ·跳躍表中刪除節點  

        在跳躍表中刪除節點爲了能夠維持跳躍表的完整性(同步跟新forward指針、backward指針和span成員)在進行實際的刪除操作之前應該按照跳躍表中查找元素的算法(forward->down)確定要刪除的節點位置在每一level的前面節點。下面以zslDelete()爲例進行介紹。

   /* node 指向要刪除的節點的指針的指針;如果不需要返回被刪除的節點,那麼直接將該參數置爲NULL即可,如果希望返回被刪除的節點,那麼該參數不能爲空
    */
  int zslDelete(zskiplist *zsl, double score, sds ele, zskiplistNode **node) {
      zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
      int i;
  
      /* 確定要刪除的節點在每一層的前面的緊鄰節點 */
      x = zsl->header;
      for (i = zsl->level-1; i >= 0; i--) {
          while (x->level[i].forward &&
                  (x->level[i].forward->score < score ||
                      (x->level[i].forward->score == score && 
                       sdscmp(x->level[i].forward->ele,ele) < 0)))
          {   
              x = x->level[i].forward;
          }
          /** update保存在該節點在每一層級的前向節點 **/
          update[i] = x;
      }
      /* We may have multiple elements with the same score, what we need
       * is to find the element with both the right score and object. */
      x = x->level[0].forward;
      if (x && score == x->score && sdscmp(x->ele,ele) == 0) {
          /* unlink from current zskiplist */
          zslDeleteNode(zsl, x, update);
          /* free the node dirctely */
          if (!node)
              zslFreeNode(x);
          else
              *node = x;
          return 1;
      }
      return 0; /* not found */
  }
 /* node 指向要刪除的節點的指針的指針;如果不需要返回被刪除的節點,那麼直接將該參數置爲NULL即可,如果希望返回被刪除的節點,那麼該參數不能爲空
    */
  int zslDelete(zskiplist *zsl, double score, sds ele, zskiplistNode **node) {
      zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
      int i;
  
      /* 確定要刪除的節點在每一層的前面的緊鄰節點 */
      x = zsl->header;
      for (i = zsl->level-1; i >= 0; i--) {
          while (x->level[i].forward &&
                  (x->level[i].forward->score < score ||
                      (x->level[i].forward->score == score && 
                       sdscmp(x->level[i].forward->ele,ele) < 0)))
          {   
              x = x->level[i].forward;
          }
          /** update保存在該節點在每一層級的前向節點 **/
          update[i] = x;
      }
      /* We may have multiple elements with the same score, what we need
       * is to find the element with both the right score and object. */
      x = x->level[0].forward;
      if (x && score == x->score && sdscmp(x->ele,ele) == 0) {
          /* unlink from current zskiplist */
          zslDeleteNode(zsl, x, update);
          /* free the node dirctely */
          if (!node)
              zslFreeNode(x);
          else
              *node = x;
          return 1;
      }
      return 0; /* not found */
  }

        在上面使用了zslDeleteNode(),下面我們分析其源碼實現。

  void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update) {
      int i;
      for (i = 0; i < zsl->level; i++) {
          if (update[i]->level[i].forward == x) {
              /** 將x的跨度轉移到x在第i層的節點update[i]上,同時減少的跨度 1 爲 x 節點本身 **/
              update[i]->level[i].span += x->level[i].span - 1;
              /* update保存了x在各個層前面的緊鄰節點,因此需要更新update[i]->level[i].forward爲x->level[i].forward,將x從跳躍表中接觸鏈接 */
              update[i]->level[i].forward = x->level[i].forward;
          } else {
               /* 如果x在第 i 層前面並沒有別的節點指向x,那麼直接將跨度 -1 即可;無需在調整指針的指向 */
              update[i]->level[i].span -= 1;
          }
      }
      if (x->level[0].forward) {
          x->level[0].forward->backward = x->backward;
      } else {
          zsl->tail = x->backward;
      }
      /** 最高層的節點的數量永遠是最少的,因此刪除的時候可能會將level減1 **/
      /** 頭結點的level[i].forward如果變爲NULL值,證明該level已經不存在任何的節點了,將zsl->level - 1; **/
      while(zsl->level > 1 && zsl->header->level[zsl->level-1].forward == NULL)
          zsl->level--;
      zsl->length--;
  }

        上面我們說了在跳躍表中刪除單個節點的過程,但是像zremrangebyscore,zremrangebylex命令都是要刪除在給定範圍內的多個元素的,因此這裏以zremrangebyscore的實現函數zslDeleteRangeByScore爲例進行說明,其中重點說明的是如何連續刪除在一定範圍內的多個節點。至於找到第一個在給定範圍內的節點的算法,前面在介紹zslFirstInRange的時候介紹過了,原理都是一樣的。

  unsigned long zslDeleteRangeByScore(zskiplist *zsl, zrangespec *range, dict *dict) {
      zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
      unsigned long removed = 0;
      int i;
  
      x = zsl->header;
      for (i = zsl->level-1; i >= 0; i--) {
          while (x->level[i].forward && (range->minex ?
              x->level[i].forward->score <= range->min :
              x->level[i].forward->score < range->min))
                  x = x->level[i].forward;
          update[i] = x;
      }
  
      /* Current node is the last with score < or <= min. */
      x = x->level[0].forward;
  
      /* Delete nodes while in range. */
      while (x &&
             (range->maxex ? x->score < range->max : x->score <= range->max))
      {
          zskiplistNode *next = x->level[0].forward;
          /** 從前向後刪除的過程中,update中的值並不會發生變化;比如刪除第一個滿足要求的節點X只有,x->forward->backward就變成了原來x->backward *  */
          zslDeleteNode(zsl,x,update);
          dictDelete(dict,x->ele);
          zslFreeNode(x); /* Here is where x->ele is actually released. */
          removed++;
          x = next;
      }
      return removed;
  }

 

        在連續刪除的過程中,每次調用zslDeleteNOde()之後都會保持原跳躍表的完整性,將原來x->level[i]->forward位置的節點提到了原來的x的位置。所以,如果下一個節點(假設爲y)還是滿足要求,那麼y在每一level的前面緊相鄰的節點仍然是update[i],每次調用zslDeleteNode的時候update的值並不需要變化。

        以上,是個人關於redis源碼中zskiplist的實現的理解,歡迎吐槽。

發佈了29 篇原創文章 · 獲贊 23 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章