redis數據結構---quicklist

        quicklist是redis在3.2版本中加入的新的數據結構,用作redis對外提供的五種數據類型---list的底層實現。本文關於redis的講解都是基於redis-4.0.1版本的源碼。

        黃建宏老師的《redis設計與實現》第二版是針對redis 3.0源碼講解的。書中講redis的list數據結構的底層實現有兩種,壓縮列表(ziplist)和雙向鏈表(adlist,以數據結構結構所在的.c文件命名以與list進行區分)。這裏的adlist就是我們通常見到的雙向鏈表結構;關於ziplist的結構請參考文章--ziplist。按照書中的解釋,當列表中的元素的數目比較少(小於512個)並且元素的長度比較小(小於64字節)的時候,redis使用ziplist作爲list的底層實現;當其中的任何一個條件不滿足的時候,redis將ziplist編碼的list轉換成adlist編碼。redis3.0對list採用以上兩種實現是基於空間和時間效率的考慮。   

        1.當元素的長度小於64字節的時候(假設都是以字符串的形式進行存儲,沒有轉換成整數),使用ziplist存儲num個entry需要的字節長度爲以下長度之和:total = zlbytes+zltail+zllen+zlend+sizeof(entry)*num = 4 + 4 +2 + 1 + sizeof(entry) * num = 11 + sizeof(entry) * num。如果長度小於64字節,那麼一個entry的prev_entry_length最多需要5個字節,encoding域需要1個字節,再加上最多63字節數據,那麼total <= (5 + 1 + 63) * num + 11 = 69 * num + 11。如果使用雙端鏈表實現,那麼在64位機器上,需要的長度爲total = sizeof(struct list) + (sizeof(struct listNode) + 63)* num = 48 + (8 + 8 + 8 + 63 )* num = 48 + 87 * num。所以,相比之下,當元素的長度比較小的時候,使用ziplist存儲相同的數據,可以節省存儲空間;

        2.但是,爲什麼要限定壓縮列表存儲的元素個數上限是512呢?ziplist在內存中是佔據一整塊的連續內存空間,當元素的個數過多的時候,list佔據的連續內存空間較大。即使是存儲的是0-12之間的整數(每個entry需要佔據2個字節),512個元素需要大約1M的內存空間(當然這是最少的情況,按照上面的分析,存儲512個63字節長的字符串的時候可能需要35M左右的連續內存空間),那麼隨着程序的運行在內存中尋找這麼大的連續內存空間變得比較困難。即使能夠找到這麼大的空間,那麼每次修改list中元素的時候都需要重新分配這麼大的內存空間,而重新分配內存空間屬於比較耗時的操作,嚴重降低了redis的效率。因此當數據量比較大的時候採用adlist作爲list的底層實現,這樣每次插入或者刪除的時候只需要爲插入的元素(listNode)單獨分配一小塊的內存空間,並將listNode銜接到現存的list中就可以了。但是使用adlist,每次分配的內存空間比較小,如果這種操作比較頻繁,那麼容易產生內存碎片;

        綜合考慮redis的空間存儲效率和時間效率,redis在3.2版本的時候引入了quicklist作爲list的底層實現,quicklist同時使用adlist和ziplist兩種數據結構作爲list的底層實現,用於緩解單獨使用ziplist或者adlist時的缺陷,quicklist在內存中的佈局如圖1所示。

圖 1 quicklist在內存中的佈局

        quicklist的基本思想是將一個比較大的ziplist拆分成較小的ziplist,每個ziplist存儲較少個數或者是較小連續內存的數據。既然是將一個很大的ziplist拆分成若干個較小的ziplist,每個ziplist應該保持多大才比較合適?

        ·如果ziplist佔用的連續內存過大或者是ziplist中entry的數目過多,那麼quicklist退化成單純使用ziplist的情況。一個極端的情況就是,整個list使用單個quicklistNode指向單個ziplist。

        ·如果ziplist中佔用的連續內存過小或者是ziplist中entry的數目過少,那麼quicklist退化成單純使用adlist的情況。一個極端的情況就是,每個ziplist中只保存一個佔用空間很小的entry(保存0-12之間的整數)。

        如果不能爲每個quicklistNode指向的ziplist確定合適的大小,那麼並不能充分揮發quicklist數據結構帶來的優勢。針對每個ziplist的大小,redis.conf文件中有兩個配置選項是與之相關的,分別是list-max-ziplist-size和list-compress-depth。

        list-max-ziplist-size指定每個ziplist佔據的內存字節長度的上限(負整數)或者是每個ziplist中entry的個數(正整數),二者選其一。在redis.conf中關於指定每個ziplist佔用的內存上限有以下幾種選項。

        · -5: 每個ziplist佔用的內存不超過64kb;

        · -4: 每個ziplist佔用的內存不超過32kb;

        · -3: 每個ziplist佔用的內存不超過16kb;

        · -2: 每個ziplist佔用的內存不超過8kb;

        · -1: 每個ziplist佔用的內存不超過4kb;

        其中,如果要使用以上幾種選項的話,redis.conf文件中建議使用-1或者-2作爲最佳選擇(系統頁的大小爲4kb或者是8kb),但是對於16kb、32kb或者64kb的大小,redis.conf不建議使用,應該是基於CPU效率的考慮。在quicklist.c文件中存在以下宏定義判斷一個ziplist中是否還繼續允許插入新的entry。

#define SIZE_SAFETY_LIMIT 8192
#define sizeMeetsSafetyLimit(sz) ((sz) <=  SIZE_SAFETY_LIMIT)

        如果redis.conf中配置的list-max-ziplist-size的值爲正整數,該數值指定了一個ziplist中允許存在的entry的最大數目。例如,redis.conf文件中默認指定的list-max-ziplist-size的值爲3,在每個ziplist中允許存在的entry的最大數目爲3個,且3個entry的長度之和不能超過8kb,需要滿足sizeMeetsSafetyLimit函數的要求。

        爲了節省內存空間,quicklist的實現中允許對每個ziplist進行壓縮,但是ziplist是否能夠壓縮,需要看是否能夠滿足壓縮的條件,這個在介紹__quicklistCompressNode的時候解釋。現在先解釋redis.conf文件中關於quicklistNode的另一個配置參數---list-compress-depth。list-compress-depth指定在每個list中的兩端沒有被壓縮的節點的個數。

    ·list-compress-depth = 1, quicklist的兩端各有1個節點不被壓縮;

    ·list-compress-depth = 2, quicklist的兩端各有2個節點不被壓縮;

    ·list-compress-depth = 3, quicklist的兩端各有3個節點不被壓縮;

    ·list-compress-depth = N, 依此類推;

          作爲一種鏈表結構,list經常執行的操作是在鏈表的頭部或者尾部執行插入或者刪除操作,因此list的頭部節點和尾部節點總是不被壓縮的list-compress-depth = 0,代表整個quicklist都不被壓縮

        quicklist主要由四種數據結構實現:quicklist、quicklistNode、quicklistLZF以及ziplist。下面分別介紹一下前三種數據結構。

 typedef struct quicklist {
      /* 整個鏈表的頭部節點 */
      quicklistNode *head;
      /* 整個鏈表的尾部節點 */
      quicklistNode *tail;
      /* 整個鏈表中的所有的entry的數量 */
      unsigned long count;
      /* quicklistNode的數量 */
      unsigned int len;
      /* 用戶存放配置文件中的list-max-ziplist-size的值 */
      int fill : 16; 
      /* 壓縮節點的深度,表示表頭或者表尾有幾個節點不被壓縮;
       * 存放list-compress-depth參數的值
       */
      unsigned int compress : 16; 
  } quicklist;
 typedef struct quicklistNode {
      /* 指向前面的quicklistNode */
      struct quicklistNode *prev;
      /* 指向後面的quicklistNode */
      struct quicklistNode *next;
      /* 如果對數據進行壓縮,zl指向ziplist;
       * 如果不對數據進行壓縮,zl指向quicklistLZF
       */
      unsigned char *zl;
      /* zl指向的ziplist的總大小(包括zlbytes,zltail,zllen,zlend和各個entry的長度之和);
       * 如果ziplist被壓縮了,那麼這個sz值仍然是表示壓縮前的ziplist的大小;
       * ziplist沒有被壓縮時,sz表示的就是ziplist佔據的字節數
       */
      unsigned int sz;
      /* ziplist內部entry的個數 */
      unsigned int count : 16;
      /* 是否可以對ziplist的壓縮;
       * encoding = QUICKLIST_NODE_ENCODING_LZF,表示可以壓縮;
       * encoding = QUICKLIST_NODE_ENCODING_RAW, 表示不壓縮;
       */
      unsigned int encoding : 2;
      /* quicklistNode是否作爲一個容器使用;
       * 如果quicklistNode不作爲容器使用,container = QUICKLIST_NODE_CONTAINER_NONE;
       * 如果quicklistNode作爲容器使用, container = QUICKLIST_NODE_CONTAINER_ZIPLIST;
       * 設計quicklist的目的就是爲了避免單獨使用adlist和ziplist,所以quicklistNode一般
       * 用作容器,指向一個包含少量entry的ziplist或者是quicklistLZF
       */
      unsigned int container : 2;  /* NONE==1 or ZIPLIST==2 */
      /* 當使用lindex這樣的命令查看某一項本來壓縮的數據的時候,需要把數據暫時解壓,
       * 這時就設置recompress=1做一個標記,等有機會再把數據進行壓縮
       */
      unsigned int recompress : 1;
      /* 只用於redis的自動化測試 */
      unsigned int attempted_compress : 1;
      unsigned int extra : 10;
  } quicklistNode;       
  typedef struct quicklistLZF {
      /* ziplist壓縮之後的存儲在compressed中的數據長度;
       * 壓縮之前的數據的長度存儲在quicklistNode的成員sz中
       */
      unsigned int sz; 
      /* 柔性數組 */
      char compressed[];
  } quicklistLZF;

        其他的輔助性的結構體的定義分別爲quicklistIter,用於在quicklist中迭代entry(從後向前或者是從前向後);還有quicklistEntry,代表ziplist中某個entry的有關信息。

  typedef struct quicklistIter {
      const quicklist *quicklist;
      /* 指向當前的zi所在的quicklistNode */
      quicklistNode *current;
      /* 指向quicklistNode中某個entry的開始地址 */
      unsigned char *zi;
      /* zi在當前quicklistNode中的偏移量 */
      long offset;
      /* 向前還是向後 */
      int direction;
  } quicklistIter;
  typedef struct quicklistEntry {
      const quicklist *quicklist;
      /* 指向當前節點的指針 */
      quicklistNode *node;
      /* 指向在entry在ziplist中的開始地址,就是該entry的prev_entry_length在ziplist中的地址 */
      unsigned char *zi;
      /* 如果ziplist中的entry的編碼格式是字符串,則value指向ziplist中entry的實際值 */
      unsigned char *value;
      /* 如果ziplist中,該entry的編碼格式是整數的形式,longval被賦予entry中的實際值*/
      long long longval;
      /* sz代表在zi指向的entry中,字符串值的長度 */
      unsigned int sz; 
      /* 當前zi指向的entry在當前ziplist的偏移量 */
      int offset;
  } quicklistEntry;

        在介紹了redis中實現quicklist的幾種重要數據結構之後,針對自己在閱讀redis的源碼的過程中認爲比較困難或者比較重要的函數加以註釋解釋,至於比較比較簡單、或者不涉及到quicklist設計技巧的輔助性函數,請閱讀redis源碼的quicklist.c文件。

        quicklist的思想是將一個很大的ziplist拆分成若干個較小的ziplist,那麼當插入一個entry到現有的quicklistNode中的時候,必須根據list-max-ziplist-size的值判斷quicklistNode指向的ziplist是否能夠繼續插入新的entry,由_quicklistNodeAllowInsert()完成該任務。

  REDIS_STATIC int _quicklistNodeAllowInsert(const quicklistNode *node,
                                             const int fill, const size_t sz) {
      if (unlikely(!node))
          return 0;
  
      int ziplist_overhead;
      /* 這種估計方式即使不考慮連鎖更新帶來的影響,那麼也同樣存在缺陷之處:
       * 後一個節點的prev_entry_length域的長度不僅僅包含了encoding和實際數據的長度,
       * 同樣也包含了本節點的prev_entry_length域的長度;
       * 在本函數的估計方法中,只是用實際數據的長度估計後一個節點的prev_entry_length域的長度
       */
      /* 計算prev_entry_length域需要的字節數 */
      if (sz < 254)
          ziplist_overhead = 1;
      else
          ziplist_overhead = 5;
  
      /* encoding域需要的長度 */
      if (sz < 64)
          ziplist_overhead += 1;
      else if (likely(sz < 16384))
          ziplist_overhead += 2;
      else
          ziplist_overhead += 5;
  
      /* 這只是一種大概的估計,node的已有長度+新增節點的長度;
       * 這種估計忽略了級聯更新帶來的影響;
       */
      unsigned int new_sz = node->sz + sz + ziplist_overhead;
      /* fill爲負數的時候,該節點已有數據的長度加上新插入的數據的長度是否能夠繼續存儲在當前quicklistNode指向的ziplist結構中*/
      if (likely(_quicklistNodeSizeMeetsOptimizationRequirement(new_sz, fill)))
          return 1;
      /* 在沒有滿足if的條件下,防止單個節點的長度過大 */
      else if (!sizeMeetsSafetyLimit(new_sz))
          return 0;
      /* fill爲正數的時候,防止節點的數目過多 */
      else if ((int)node->count < fill)
          return 1;
      else
          return 0;
  }

    其中,關於_quicklistNodeSizeMeetsOptimizationRequirement()主要是是根據要插入的元素的sz以及配置的list-max-ziplist-size確定是否允許插入,其實現如下。

  REDIS_STATIC int
  _quicklistNodeSizeMeetsOptimizationRequirement(const size_t sz,
                                                 const int fill) {
      if (fill >= 0)
          return 0;
  
      /* 確定在限制ziplist大小的數組中的偏移量 */
      size_t offset = (-fill) - 1;
      if (offset < (sizeof(optimization_level) / sizeof(*optimization_level))) {
          if (sz <= optimization_level[offset]) {
              return 1;
          } else {
              return 0;
          }
      } else {
          return 0;
      }
  }

    redis.conf文件中的redis-compress-depth配置參數決定是否以及如何對ziplist數據結構進行壓縮,但是,redis究竟是否真的壓縮一個quicklistNode指向的ziplist,還要看是否滿足壓縮的條件,具體由__quicklistCompressNode(quicklistNode *node)實現。

  REDIS_STATIC int __quicklistCompressNode(quicklistNode *node) {
  #ifdef REDIS_TEST
      node->attempted_compress = 1;
  #endif
  
      /* #define MIN_COMPRESS_BYTES 48
       * 允許壓縮的ziplist的最小長度,小於這個值不執行壓縮
       */
      if (node->sz < MIN_COMPRESS_BYTES)
          return 0;
  
      /* 爲壓縮之後的空間分配足夠的大小 */
      quicklistLZF *lzf = zmalloc(sizeof(*lzf) + node->sz);
  
      /* 如果壓縮失敗(壓縮過程本身出錯或者是傳入的壓縮之後的空間大小不滿足要求)
       * 或者是壓縮之後節省的空間小於MIN_COMPRESS_IMPROVE,不執行壓縮
       */
      if (((lzf->sz = lzf_compress(node->zl, node->sz, lzf->compressed,
                                   node->sz)) == 0) ||
          lzf->sz + MIN_COMPRESS_IMPROVE >= node->sz) {
          zfree(lzf);
          return 0;
      }
      /* 去掉壓縮之後的lzf中多餘的空間大小 */
      lzf = zrealloc(lzf, sizeof(*lzf) + lzf->sz);
      /* 釋放掉原來的ziplist佔據的空間大小 */
      zfree(node->zl);
      node->zl = (unsigned char *)lzf;
      node->encoding = QUICKLIST_NODE_ENCODING_LZF;
      node->recompress = 0;
      return 1;
  }

    既然存在壓縮,那麼肯定存在解壓,將quicklistLZF結構的數據解壓到ziplist結構,__quicklistDecompressNode就是完成這個功能的,如下。

  REDIS_STATIC int __quicklistDecompressNode(quicklistNode *node) {
  #ifdef REDIS_TEST
      node->attempted_compress = 0; 
  #endif
  
      void *decompressed = zmalloc(node->sz);
      quicklistLZF *lzf = (quicklistLZF *)node->zl;
      if (lzf_decompress(lzf->compressed, lzf->sz, decompressed, node->sz) == 0) { 
          zfree(decompressed);
          return 0;
      }    
      zfree(lzf);
      node->zl = decompressed;
      node->encoding = QUICKLIST_NODE_ENCODING_RAW;
      return 1;
  }

    以上,是關於redis對quicklist設計思想以及redis.conf文件中對於list-max-ziplist-size和list-compress-depth參數的講解。下面,剖析list的增、刪、改、查調用的quicklist的接口。

    在quicklist的頭部增加entry。

  int quicklistPushHead(quicklist *quicklist, void *value, size_t sz) {
      quicklistNode *orig_head = quicklist->head;
      if (likely(
              _quicklistNodeAllowInsert(quicklist->head, quicklist->fill, sz))) {
          quicklist->head->zl =
              ziplistPush(quicklist->head->zl, value, sz, ZIPLIST_HEAD);
          quicklistNodeUpdateSz(quicklist->head);
      } else {
          quicklistNode *node = quicklistCreateNode();
          node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_HEAD);
  
          quicklistNodeUpdateSz(node);
          /* 將新增加的節點添加到列表的quicklist的頭部 */
          _quicklistInsertNodeBefore(quicklist, quicklist->head, node);
      }
      quicklist->count++;
      quicklist->head->count++;
      return (orig_head != quicklist->head);
  }

    在quicklist的尾部增加entry。

  int quicklistPushTail(quicklist *quicklist, void *value, size_t sz) {
      quicklistNode *orig_tail = quicklist->tail;
      if (likely(
              _quicklistNodeAllowInsert(quicklist->tail, quicklist->fill, sz))) {
          quicklist->tail->zl =
              ziplistPush(quicklist->tail->zl, value, sz, ZIPLIST_TAIL);
          quicklistNodeUpdateSz(quicklist->tail);
      } else {
          quicklistNode *node = quicklistCreateNode();
          node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_TAIL);
  
          quicklistNodeUpdateSz(node);
          _quicklistInsertNodeAfter(quicklist, quicklist->tail, node);
      }
      quicklist->count++;
      quicklist->tail->count++;
      return (orig_tail != quicklist->tail);
  }

    在某個指定的quicklistEntry之前或者之後增加一個新的entry。after=0,表示在傳入的entry之前插入新的entry;after=1表示在傳入的entry之後插入新的entry。

REDIS_STATIC void _quicklistInsert(quicklist *quicklist, quicklistEntry *entry,
                                     void *value, const size_t sz, int after) {
      int full = 0, at_tail = 0, at_head = 0, full_next = 0, full_prev = 0;
      int fill = quicklist->fill;
      quicklistNode *node = entry->node;
      quicklistNode *new_node = NULL;
  
      if (!node) {
          /* we have no reference node, so let's create only node in the list */
          D("No node given!");
          new_node = quicklistCreateNode();
          new_node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_HEAD);
          /* __quicklistInsertNode中在增加node的同時已經將node的數量增加過了 */
          __quicklistInsertNode(quicklist, NULL, new_node, after);
          /* 增加當前node中ziplist中的entry的數量 */
          new_node->count++;
          /* 增加整個quicklist中entry的數量 */
          quicklist->count++;
          return;
      }
  
      /* 插入的策略是,如果當前的節點允許進行插入,那麼直接在當前的節點中進行插入;
       * 否則根據after的值決定是在node->next中插入還是在node->prev中插入
       */
      /* Populate accounting flags for easier boolean checks later */
      /* 判斷當前的節點是否允許插入 */
      if (!_quicklistNodeAllowInsert(node, fill, sz)) {
          D("Current node is full with count %d with requested fill %lu",
            node->count, fill);
          full = 1;
      }
     /* 一般情況下,entry->offset <= node->count - 1,但是在執行ziplistDelEntry之後,
       * 可能offset的值爲node->count,這種情況下執行quicklistNext的時候,
       * 會跳轉到下一個quicklistNode
       */
      if (after && (entry->offset == node->count)) {
          D("At Tail of current ziplist");
          at_tail = 1;
          if (!_quicklistNodeAllowInsert(node->next, fill, sz)) {
              D("Next node is full too.");
              full_next = 1;
          }
      }
  
      /* 如果當前的entry爲某個node中最前面的一個,並且新添加的entry要位於該entry之前,
       * 那麼需要判斷前一個節點之中是否再次允許添加entry
       */
      if (!after && (entry->offset == 0)) {
          D("At Head");
          at_head = 1;
          if (!_quicklistNodeAllowInsert(node->prev, fill, sz)) {
              D("Prev node is full too.");
              full_prev = 1;
          }
      }
  
      /* Now determine where and how to insert the new element */
      /* 如果當前節點允許插入新的entry,則直接在entry的後面插入 */
      if (!full && after) {
          D("Not full, inserting after current position.");
          quicklistDecompressNodeForUse(node);
          /* 確定新的entry要插入的位置 */
          unsigned char *next = ziplistNext(node->zl, entry->zi);
          if (next == NULL) {
              node->zl = ziplistPush(node->zl, value, sz, ZIPLIST_TAIL);
          } else {
              node->zl = ziplistInsert(node->zl, next, value, sz);
          }
          node->count++;
          quicklistNodeUpdateSz(node);
          quicklistRecompressOnly(quicklist, node);
      /* 如果當前節點允許插入新的entry,那麼當前entry所在的位置就應該是插入的位置 */
      } else if (!full && !after) {
          D("Not full, inserting before current position.");
          quicklistDecompressNodeForUse(node);
          node->zl = ziplistInsert(node->zl, entry->zi, value, sz);
          node->count++;
          quicklistNodeUpdateSz(node);
          quicklistRecompressOnly(quicklist, node);
      /* 如果滿足以下條件,那麼在當前node的下一個node中的ziplist的頭部插入新的entry:
       * 1.當前ziplist中不允許插入新的entry,也就是full爲1;
       * 2.要插入的位置爲當前的node的最後一個位置,也就是at_tail == 1;
       * 3.node的下一個節點存在,並且下一個節點允許插入新的entry;
       * 4.是要在尾部插入,但是個人感覺這項條件不必放在這裏,因爲at_tail已經表明after爲真;
       */
      /* 只有要在當前節點的尾部進行插入,並且下一個節點允許插入的時候纔在下一個node對應的
       * ziplist的頭部進行插入
       */
      } else if (full && at_tail && node->next && !full_next && after) {
          /* If we are: at tail, next has free space, and inserting after:
           *   - insert entry at head of next node. */
          D("Full and tail, but next isn't full; inserting next node head");
          new_node = node->next;
          quicklistDecompressNodeForUse(new_node);
          new_node->zl = ziplistPush(new_node->zl, value, sz, ZIPLIST_HEAD);
          new_node->count++;
          quicklistNodeUpdateSz(new_node);
          quicklistRecompressOnly(quicklist, new_node);
      /* 與上一個一樣,只不過這裏是要在當前node對應的ziplist的頭部進行插入的時候,
       * 如果上一個node對應的ziplist中允許插入新的entry,在上一個節點對應的ziplist的
       * 尾部插入新的entry
       */
      } else if (full && at_head && node->prev && !full_prev && !after) {
          /* If we are: at head, previous has free space, and inserting before:
           *   - insert entry at tail of previous node. */
          D("Full and head, but prev isn't full, inserting prev node tail");
          new_node = node->prev;
          quicklistDecompressNodeForUse(new_node);
          new_node->zl = ziplistPush(new_node->zl, value, sz, ZIPLIST_TAIL);
          new_node->count++;
          quicklistNodeUpdateSz(new_node);
          quicklistRecompressOnly(quicklist, new_node);
      /* 如果滿足以下條件,那麼創建新的quicklistNode,將entry插入到新創建的quicklistNode中,
       * 並根據after的值決定將新創建的quicklistNode鏈接到當前node的前面還是後面:
       * 1.當前節點不允許插入要插入的entry;
       * 2.如果要插入到當前node的最後位置,但是node->next存在並且已經不允許插入
       *   或者要插入到當前node最開始的位置,但是node->prev存在並且已經不允許插入
       */
      } else if (full && ((at_tail && node->next && full_next && after) ||
                          (at_head && node->prev && full_prev && !after))) {
          /* If we are: full, and our prev/next is full, then:
           *   - create new node and attach to quicklist */
          D("\tprovisioning new node...");
          new_node = quicklistCreateNode();
          new_node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_HEAD);
          new_node->count++;
          quicklistNodeUpdateSz(new_node);
          __quicklistInsertNode(quicklist, node, new_node, after);
      /* 如果要插入的位置在當前node的中間位置(不是最前面也不是最後面)且當前的node已滿,
       * 不允許插入新的entry,則在當前entry所在的位置將node分裂
       */
      } else if (full) {
          /* else, node is full we need to split it. */
          /* covers both after and !after cases */
          D("\tsplitting node...");
          quicklistDecompressNodeForUse(node);
          /* 根據當前entry的offset將node對應的ziplist分裂成兩個quicklistNode,
           * after=1的時候,返回的是[offset+1, end],此時在new_node對應的ziplist的頭部進行插入;
           * after=0的時候,返回的是[0, offset-1],此時在new_node對應的ziplist的尾部插入;
           */
          new_node = _quicklistSplitNode(node, entry->offset, after);
          new_node->zl = ziplistPush(new_node->zl, value, sz,
                                     after ? ZIPLIST_HEAD : ZIPLIST_TAIL);
          new_node->count++;
          quicklistNodeUpdateSz(new_node);
          /* 將分裂出的節點根據after的值,確定是放在輸入節點的前面還是放在輸入節點的後面 */
          __quicklistInsertNode(quicklist, node, new_node, after);
          /* else處理的是當前節點不能在entry的之前或者之後直接插入,並且要插入的位置不是在當前node對應的ziplist的最開始或者最後的時候,
           * 此時要對ziplistNode進行分裂,分裂之後的new_node可能可以和node->prev或者node->next進行合併,
           * 所以執行_quicklistMergeNodes
           */
          _quicklistMergeNodes(quicklist, node);
      }
  
      quicklist->count++;
  }

    刪除從偏移位置start開始的count個節點。

  int quicklistDelRange(quicklist *quicklist, const long start,
                        const long count) {
      if (count <= 0)
          return 0;
  
      unsigned long extent = count; /* range is inclusive of start position */
  
      /* 對從start開始的超出ziplist的長度限制的情況進行約束 */
      if (start >= 0 && extent > (quicklist->count - start)) {
          /* if requesting delete more elements than exist, limit to list size. */
          extent = quicklist->count - start;
      } else if (start < 0 && extent > (unsigned long)(-start)) {
          /* else, if at negative offset, limit max size to rest of list. */
          extent = -start; /* c.f. LREM -29 29; just delete until end. */
      }
  
      quicklistEntry entry;
      /* 如果start索引位置的entry並不存在,則quicklistIndex返回的值爲0,表示沒有這個entry;
       * 如果start索引位置的entry存在,則quicklistIndex返回的值爲1,表示這個entry是存在的,
       * 這樣能夠保證以後對這個entry的所有操作都是合理的
       */
      if (!quicklistIndex(quicklist, start, &entry))
          return 0;
  
      D("Quicklist delete request for start %ld, count %ld, extent: %ld", start,
        count, extent);
      quicklistNode *node = entry.node;
  
      /* iterate over next nodes until everything is deleted. */
      while (extent) {
          quicklistNode *next = node->next;
  
          unsigned long del; 
          int delete_entire_node = 0; 
          /* 刪除當前的整個節點的情況;可能還需要刪除掉其餘節點中的entry */
          if (entry.offset == 0 && extent >= node->count) {
              /* If we are deleting more than the count of this node, we
               * can just delete the entire node without ziplist math. */
              delete_entire_node = 1;
              del = node->count;
          /* else if 中肯定不存在entry.offset == 0 並且 extent >= node->count 的情況;
             因此,肯定不存在刪除整個quicklistNode的情況
           */
          /* 個人認爲這裏存在問題,正確的判斷條件應該是(entry.offset >=0 && extent >= node->count - entry.offset。舉個例子,假如每個quicklist  Node中最多允許存在9個entry,現在一共有3個quicklistNode,且每個quicklistNode中都存在9個entry。當start=6,count=6時,直接進入了最後了else,del變
  爲6,在第一個quicklisNode指向的ziplist中從offset=6開始刪除6個元素,但是此時很顯然只能刪除4個元素,ziplistDelteRange並不能分辨出這種錯誤
           * 歡迎吐槽這個疑問!!!
           */
          } else if (entry.offset >= 0 && extent >= node->count) {
              /* If deleting more nodes after this one, calculate delete based
               * on size of current node. */
              del = node->count - entry.offset;
          } else if (entry.offset < 0) {
              /* If offset is negative, we are in the first run of this loop
               * and we are deleting the entire range
               * from this start offset to end of list.  Since the Negative
               * offset is the number of elements until the tail of the list,
               * just use it directly as the deletion count. */
              del = -entry.offset;
  
              /* If the positive offset is greater than the remaining extent,
               * we only delete the remaining extent, not the entire offset.
               */
              if (del > extent)
                  del = extent;
          } else {
              /* else, we are deleting less than the extent of this node, so
               * use extent directly. */
              del = extent;
          }
  
          D("[%ld]: asking to del: %ld because offset: %d; (ENTIRE NODE: %d), "
            "node count: %u",
            extent, del, entry.offset, delete_entire_node, node->count);
  
          if (delete_entire_node) {
              __quicklistDelNode(quicklist, node);
          } else {
              /* 解壓當前的quicklistNode用於刪除quciklistNode指向的ziplist中指定的entry */
              quicklistDecompressNodeForUse(node);
              node->zl = ziplistDeleteRange(node->zl, entry.offset, del);
              quicklistNodeUpdateSz(node);
              node->count -= del;
              quicklist->count -= del;
              /* quicklistDeleteIfEmpty在釋放掉node指向的內存空間之後,將node置爲NULL,
               * 因此在下面可以對node是否NULL進行判斷;僅僅是free的話並不會將指針置爲NULL
               */
              quicklistDeleteIfEmpty(quicklist, node);
              if (node)
                  quicklistRecompressOnly(quicklist, node);
          }
  
          extent -= del;
  
          node = next;
  
          entry.offset = 0;
      }
      return 1;
  }

    對於鏈表而言,經常做的操作是從鏈表的頭部或者是尾部進行刪除操作。

  /* 返回爲0,表示沒有沒有找到相應的entry;返回1,表示存在相應的entry */
  int quicklistPop(quicklist *quicklist, int where, unsigned char **data,
                   unsigned int *sz, long long *slong) {
      unsigned char *vstr;
      unsigned int vlen;
      long long vlong;
      if (quicklist->count == 0)
          return 0;
      int ret = quicklistPopCustom(quicklist, where, &vstr, &vlen, &vlong,
                                   _quicklistSaver);
      if (data)
          *data = vstr;
      if (slong)
          *slong = vlong;
      if (sz)
          *sz = vlen;
      return ret;
  }


  int quicklistPopCustom(quicklist *quicklist, int where, unsigned char **data,
                         unsigned int *sz, long long *sval,
                         void *(*saver)(unsigned char *data, unsigned int sz)) {
      unsigned char *p;
      unsigned char *vstr;
      unsigned int vlen;
      long long vlong;
      int pos = (where == QUICKLIST_HEAD) ? 0 : -1;
  
      if (quicklist->count == 0)
          return 0;
  
      /* 對傳入的參數進行初始化,data用來存儲set,sz表示data的長度;
       * 當只是數值的時候,直接用sval來存儲
       */
      if (data)
          *data = NULL;
      if (sz)
          *sz = 0;
      if (sval)
          *sval = -123456789;
  
      quicklistNode *node;
      if (where == QUICKLIST_HEAD && quicklist->head) {
          node = quicklist->head;
      } else if (where == QUICKLIST_TAIL && quicklist->tail) {
          node = quicklist->tail;
      } else {
          return 0;
      }
      p = ziplistIndex(node->zl, pos);
      if (ziplistGet(p, &vstr, &vlen, &vlong)) {
          if (vstr) {
              if (data)
                  *data = saver(vstr, vlen);
              if (sz)
                  *sz = vlen;
          } else {
              if (data)
                  *data = NULL;
              if (sval)
                  *sval = vlong;
          }
          quicklistDelIndex(quicklist, node, &p);
          return 1;
      }
      return 0;
  }

     改,quicklist允許在指定的偏移位置用新值代替舊值。

  int quicklistReplaceAtIndex(quicklist *quicklist, long index, void *data,
                              int sz) {
      quicklistEntry entry;
      if (likely(quicklistIndex(quicklist, index, &entry))) {
          /* quicklistIndex provides an uncompressed node */
          entry.node->zl = ziplistDelete(entry.node->zl, &entry.zi);
          entry.node->zl = ziplistInsert(entry.node->zl, entry.zi, data, sz);
          quicklistNodeUpdateSz(entry.node);
          /* 判斷是否應該對entry.node進行壓縮 */
          quicklistCompress(quicklist, entry.node);
          return 1;
      } else {
          return 0;
      }
  }

    查,quicklist根據指定的偏移量確定該位置的entry。

  int quicklistIndex(const quicklist *quicklist, const long long idx,
                     quicklistEntry *entry) {
      quicklistNode *n;
      unsigned long long accum = 0;
      unsigned long long index;
      int forward = idx < 0 ? 0 : 1; /* < 0 -> reverse, 0+ -> forward */
  
      initEntry(entry);
      entry->quicklist = quicklist;
  
      if (!forward) {
          index = (-idx) - 1;
          n = quicklist->tail;
      } else {
          index = idx;
          n = quicklist->head;
      }
  
      if (index >= quicklist->count)
          return 0;
  
      /* 已經將 index > quicklist->count 的情況排除在外 */
      while (likely(n)) {
          if ((accum + n->count) > index) {
              break;
          } else {
              D("Skipping over (%p) %u at accum %lld", (void *)n, n->count,
                accum);
              accum += n->count;
              n = forward ? n->next : n->prev;
          }
      }
  
      /* quicklist中entry的數目不夠的話,可能導致n變成了NULL指針;
       * 經過上面的循環之後重新檢查n是否爲NULL。但是index >= quicklist->count的條件難道不是已經排除了這種情況了嗎?
       */
      if (!n)
          return 0;
  
      D("Found node: %p at accum %llu, idx %llu, sub+ %llu, sub- %llu", (void *)n,
        accum, index, index - accum, (-index) - 1 + accum);
  
      entry->node = n;
      if (forward) {
          /* forward = normal head-to-tail offset. */
          entry->offset = index - accum;
      } else {
          /* reverse = need negative offset for tail-to-head, so undo
           * the result of the original if (index < 0) above. */
          /* 如果是從最後向前按照索引進行查找(也就是傳入的參數中 idx < 0),
           * 傳入到ziplist中進行查找的時候也需要傳入在當前的ziplist中的負的偏移量
           */
          entry->offset = (-index) - 1 + accum;
      }
  
      /* 以下的操作中確定quicklistNode和quicklistNode中的ziplist的entry都是存在的 */
      quicklistDecompressNodeForUse(entry->node);
      entry->zi = ziplistIndex(entry->node->zl, entry->offset);
      ziplistGet(entry->zi, &entry->value, &entry->sz, &entry->longval);
      /* The caller will use our result, so we don't re-compress here.
       * The caller can recompress or delete the node as needed. */
      return 1;
  }

    當然,quicklist提供了迭代獲取每一個元素的方法,quicklistNext。

  /* 獲取quicklistIter中成員變量zi指向的entry的下一個entry,
   * 當然quicklistNext是一個自身的遞歸調用
   */
  int quicklistNext(quicklistIter *iter, quicklistEntry *entry) {
      initEntry(entry);
  
      if (!iter) {
          D("Returning because no iter!");
          return 0;
      }
  
      entry->quicklist = iter->quicklist;
      entry->node = iter->current;
  
      if (!iter->current) {
          D("Returning because current node is NULL")
          return 0;
      }
  
      unsigned char *(*nextFn)(unsigned char *, unsigned char *) = NULL;
      int offset_update = 0;
  
      /* 滿足if條件的有以下幾種情況:
       * 1.初始化一個quicklistIter之後直接調用該函數;
       * 2.調用quicklistDelEntry刪除一個node的節點之後,iter->zi被賦值爲NULL值,這裏又劃分爲兩種情況:
       *   1.刪除的是一個node的最後一個entry,那麼iter->offset並沒有發生變化(參考quicklistDelEntry的註釋)
       *     ,此時通過ziplistIndex獲取當前的entry仍然爲NULL值;
       *   2.刪除的node中不是最後一個entry,此時iter->offset並沒有發生也不用發生變化(參考quicklistDelEntry
       *     的註釋說明),那麼此時通過iter->offset調用ziplistIndex可以獲取相應的ziplist在iter->offset
       *     位置的entry;
       * 3. 函數發生遞歸調用的時候iter->zi被賦值爲NULL;
       */
      if (!iter->zi) {
          /* If !zi, use current index. 即使是經過ziplistIndex獲取iter->offset,iter->zi仍然可能爲NULL值 */
          quicklistDecompressNodeForUse(iter->current);
          iter->zi = ziplistIndex(iter->current->zl, iter->offset);
      /* 如果iter->zi指向的位置的entry是存在的,那麼獲取iter->zi的下一個位置的entry */
      } else {
          /* else, use existing iterator offset and get prev/next as necessary. */
          /* 如果iter指向的是某個node中的最後一個entry,
           * 那麼ziplistNext返回的是NULL
           */
          if (iter->direction == AL_START_HEAD) {
              nextFn = ziplistNext;
              offset_update = 1;
          /* 如果iter指向的是某個node中的最開始的一個entry,
           * 那麼ziplistPrev返回的是NULL
           */
          } else if (iter->direction == AL_START_TAIL) {
              nextFn = ziplistPrev;
              offset_update = -1;
          }
          iter->zi = nextFn(iter->current->zl, iter->zi);
          iter->offset += offset_update;
      }
  
      entry->zi = iter->zi;
      entry->offset = iter->offset;
  
      /* 如果獲取的iter->zi存在,那麼直接獲取獲取對應的entry中的值 */
      if (iter->zi) {
          /* Populate value from existing ziplist position */
          ziplistGet(entry->zi, &entry->value, &entry->sz, &entry->longval);
          return 1;
      /* 如果對應的iter->zi(爲NULL的話),根據direction參數確定下一個quicklistNode */
      } else {
          /* We ran out of ziplist entries.
           * Pick next node, update offset, then re-run retrieval. */
          quicklistCompress(iter->quicklist, iter->current);
          if (iter->direction == AL_START_HEAD) {
              /* Forward traversal */
              D("Jumping to start of next node");
              iter->current = iter->current->next;
              iter->offset = 0;
          } else if (iter->direction == AL_START_TAIL) {
              /* Reverse traversal */
              D("Jumping to end of previous node");
              iter->current = iter->current->prev;
              iter->offset = -1;
          }
          /* 適用於最開始的if條件語句中判斷iter->zi爲NULL,此時可以通過以上的iter->offset設定
           * 的值,確定新的iter->zi,並確定entry中響應的值;
           */
          iter->zi = NULL;
          return quicklistNext(iter, entry);
      }
  }

    quicklist的操作中,針對quicklistNode的操作是雙端鏈表的操作;針對每一個具體的entry的操作,直接調用redis中ziplist的實現。redis中關於雙端鏈表的實現還包括以下的函數。

    ·quicklistAppendValuesFromZiplist():實現向前的兼容性,將原來3.2版本之前的ziplist可以轉換成quicklist結構,用於讀取rdb或者aof文件,將數據加載到內存中;

    ·_quicklistSplitNode():將一個節點分裂成兩個節點,其實現如下:

  REDIS_STATIC quicklistNode *_quicklistSplitNode(quicklistNode *node, int offset,
                                                  int after) {
      size_t zl_sz = node->sz;
  
      quicklistNode *new_node = quicklistCreateNode();
      new_node->zl = zmalloc(zl_sz);
  
      /* Copy original ziplist so we can split it */
      memcpy(new_node->zl, node->zl, zl_sz);
  
      /* -1 here means "continue deleting until the list ends" */
      /* 這裏之所以說-1表示刪除掉從某個位置開始的entry中的左右數目,是因爲ziplistDeleteRange(ziplist* ziplist, unsigned char *p, unsigned int num)中num的類型爲unsigned int,但是如果傳遞-1,表
  示傳遞的是int類型,根據數據在計算機中的"補碼"表示形式,那麼實際傳遞進去的應該是0xffffffff,這個值已經足可以和zlbytes成員的上限一致了,但是每個ziplist中的entry至少需要2個字節,所以總的ziplis  t中總的數量是不可能達到0xffffffff的。所以,傳遞"-1"表示刪除掉從開始位置開始的所有的元素
       */
      /* 根據輸入的參數after確定輸入的節點node要刪除的entry的起始位置以及數量 */
      int orig_start = after ? offset + 1 : 0;
      int orig_extent = after ? -1 : offset;
      /* 根據輸入的參數after確定新創建的節點中要刪除的entry的起始位置和數量 */
      int new_start = after ? 0 : offset;
      int new_extent = after ? offset + 1 : -1;
  
      D("After %d (%d); ranges: [%d, %d], [%d, %d]", after, offset, orig_start,
        orig_extent, new_start, new_extent);
  
      node->zl = ziplistDeleteRange(node->zl, orig_start, orig_extent);
      node->count = ziplistLen(node->zl);
      quicklistNodeUpdateSz(node);
  
      new_node->zl = ziplistDeleteRange(new_node->zl, new_start, new_extent);
      new_node->count = ziplistLen(new_node->zl);
      quicklistNodeUpdateSz(new_node);
  
      D("After split lengths: orig (%d), new (%d)", node->count, new_node->count);
      return new_node;
  }

    ·_quicklistZiplistMerge():用於講兩個節點合併成一個節點;   

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

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