Redis 為什麼用跳錶而不用平衡樹?

張鐵蕾發表於2016-10-09

本文是《Redis內部資料結構詳解》系列的第六篇。在本文中,我們圍繞一個Redis的內部資料結構——skiplist展開討論。

Redis裡面使用skiplist是為了實現sorted set這種對外的資料結構。sorted set提供的操作非常豐富,可以滿足非常多的應用場景。這也意味著,sorted set相對來說實現比較複雜。同時,skiplist這種資料結構對於很多人來說都比較陌生,因為大部分學校裡的演算法課都沒有對這種資料結構進行過詳細的介紹。因此,為了介紹得足夠清楚,本文會比這個系列的其它幾篇花費更多的篇幅。

我們將大體分成三個部分進行介紹:

  1. 介紹經典的skiplist資料結構,並進行簡單的演算法分析。這一部分的介紹,與Redis沒有直接關係。我會嘗試儘量使用通俗易懂的語言進行描述。
  2. 討論Redis裡的skiplist的具體實現。為了支援sorted set本身的一些要求,在經典的skiplist基礎上,Redis裡的相應實現做了若干改動。
  3. 討論sorted set是如何在skiplist, dict和ziplist基礎上構建起來的。

我們在討論中還會涉及到兩個Redis配置(在redis.conf中的ADVANCED CONFIG部分):

zset-max-ziplist-entries 128
zset-max-ziplist-value 64複製程式碼

我們在討論中會詳細解釋這兩個配置的含義。

注:本文討論的程式碼實現基於Redis原始碼的3.2分支。

skiplist資料結構簡介

skiplist本質上也是一種查詢結構,用於解決演算法中的查詢問題(Searching),即根據給定的key,快速查到它所在的位置(或者對應的value)。

我們在《Redis內部資料結構詳解》系列的第一篇中介紹dict的時候,曾經討論過:一般查詢問題的解法分為兩個大類:一個是基於各種平衡樹,一個是基於雜湊表。但skiplist卻比較特殊,它沒法歸屬到這兩大類裡面。

這種資料結構是由William Pugh發明的,最早出現於他在1990年發表的論文《Skip Lists: A Probabilistic Alternative to Balanced Trees》。對細節感興趣的同學可以下載論文原文來閱讀。

skiplist,顧名思義,首先它是一個list。實際上,它是在有序連結串列的基礎上發展起來的。

我們先來看一個有序連結串列,如下圖(最左側的灰色節點表示一個空的頭結點):

Redis 為什麼用跳錶而不用平衡樹?

在這樣一個連結串列中,如果我們要查詢某個資料,那麼需要從頭開始逐個進行比較,直到找到包含資料的那個節點,或者找到第一個比給定資料大的節點為止(沒找到)。也就是說,時間複雜度為O(n)。同樣,當我們要插入新資料的時候,也要經歷同樣的查詢過程,從而確定插入位置。

假如我們每相鄰兩個節點增加一個指標,讓指標指向下下個節點,如下圖:

Redis 為什麼用跳錶而不用平衡樹?

這樣所有新增加的指標連成了一個新的連結串列,但它包含的節點個數只有原來的一半(上圖中是7, 19, 26)。現在當我們想查詢資料的時候,可以先沿著這個新連結串列進行查詢。當碰到比待查資料大的節點時,再回到原來的連結串列中進行查詢。比如,我們想查詢23,查詢的路徑是沿著下圖中標紅的指標所指向的方向進行的:

Redis 為什麼用跳錶而不用平衡樹?

  • 23首先和7比較,再和19比較,比它們都大,繼續向後比較。
  • 但23和26比較的時候,比26要小,因此回到下面的連結串列(原連結串列),與22比較。
  • 23比22要大,沿下面的指標繼續向後和26比較。23比26小,說明待查資料23在原連結串列中不存在,而且它的插入位置應該在22和26之間。

在這個查詢過程中,由於新增加的指標,我們不再需要與連結串列中每個節點逐個進行比較了。需要比較的節點數大概只有原來的一半。

利用同樣的方式,我們可以在上層新產生的連結串列上,繼續為每相鄰的兩個節點增加一個指標,從而產生第三層連結串列。如下圖:

Redis 為什麼用跳錶而不用平衡樹?

在這個新的三層連結串列結構上,如果我們還是查詢23,那麼沿著最上層連結串列首先要比較的是19,發現23比19大,接下來我們就知道只需要到19的後面去繼續查詢,從而一下子跳過了19前面的所有節點。可以想象,當連結串列足夠長的時候,這種多層連結串列的查詢方式能讓我們跳過很多下層節點,大大加快查詢的速度。

skiplist正是受這種多層連結串列的想法的啟發而設計出來的。實際上,按照上面生成連結串列的方式,上面每一層連結串列的節點個數,是下面一層的節點個數的一半,這樣查詢過程就非常類似於一個二分查詢,使得查詢的時間複雜度可以降低到O(log n)。但是,這種方法在插入資料的時候有很大的問題。新插入一個節點之後,就會打亂上下相鄰兩層連結串列上節點個數嚴格的2:1的對應關係。如果要維持這種對應關係,就必須把新插入的節點後面的所有節點(也包括新插入的節點)重新進行調整,這會讓時間複雜度重新蛻化成O(n)。刪除資料也有同樣的問題。

skiplist為了避免這一問題,它不要求上下相鄰兩層連結串列之間的節點個數有嚴格的對應關係,而是為每個節點隨機出一個層數(level)。比如,一個節點隨機出的層數是3,那麼就把它鏈入到第1層到第3層這三層連結串列中。為了表達清楚,下圖展示瞭如何通過一步步的插入操作從而形成一個skiplist的過程(點選看大圖):

Redis 為什麼用跳錶而不用平衡樹?

從上面skiplist的建立和插入過程可以看出,每一個節點的層數(level)是隨機出來的,而且新插入一個節點不會影響其它節點的層數。因此,插入操作只需要修改插入節點前後的指標,而不需要對很多節點都進行調整。這就降低了插入操作的複雜度。實際上,這是skiplist的一個很重要的特性,這讓它在插入效能上明顯優於平衡樹的方案。這在後面我們還會提到。

根據上圖中的skiplist結構,我們很容易理解這種資料結構的名字的由來。skiplist,翻譯成中文,可以翻譯成“跳錶”或“跳躍表”,指的就是除了最下面第1層連結串列之外,它會產生若干層稀疏的連結串列,這些連結串列裡面的指標故意跳過了一些節點(而且越高層的連結串列跳過的節點越多)。這就使得我們在查詢資料的時候能夠先在高層的連結串列中進行查詢,然後逐層降低,最終降到第1層連結串列來精確地確定資料位置。在這個過程中,我們跳過了一些節點,從而也就加快了查詢速度。

剛剛建立的這個skiplist總共包含4層連結串列,現在假設我們在它裡面依然查詢23,下圖給出了查詢路徑:

Redis 為什麼用跳錶而不用平衡樹?

需要注意的是,前面演示的各個節點的插入過程,實際上在插入之前也要先經歷一個類似的查詢過程,在確定插入位置後,再完成插入操作。

至此,skiplist的查詢和插入操作,我們已經很清楚了。而刪除操作與插入操作類似,我們也很容易想象出來。這些操作我們也應該能很容易地用程式碼實現出來。

當然,實際應用中的skiplist每個節點應該包含key和value兩部分。前面的描述中我們沒有具體區分key和value,但實際上列表中是按照key進行排序的,查詢過程也是根據key在比較。

但是,如果你是第一次接觸skiplist,那麼一定會產生一個疑問:節點插入時隨機出一個層數,僅僅依靠這樣一個簡單的隨機數操作而構建出來的多層連結串列結構,能保證它有一個良好的查詢效能嗎?為了回答這個疑問,我們需要分析skiplist的統計效能。

在分析之前,我們還需要著重指出的是,執行插入操作時計算隨機數的過程,是一個很關鍵的過程,它對skiplist的統計特性有著很重要的影響。這並不是一個普通的服從均勻分佈的隨機數,它的計算過程如下:

  • 首先,每個節點肯定都有第1層指標(每個節點都在第1層連結串列裡)。
  • 如果一個節點有第i層(i>=1)指標(即節點已經在第1層到第i層連結串列中),那麼它有第(i+1)層指標的概率為p。
  • 節點最大的層數不允許超過一個最大值,記為MaxLevel。

這個計算隨機層數的偽碼如下所示:

randomLevel()
    level := 1
    // random()返回一個[0...1)的隨機數
    while random() < p and level < MaxLevel do
        level := level + 1
    return level複製程式碼

randomLevel()的偽碼中包含兩個引數,一個是p,一個是MaxLevel。在Redis的skiplist實現中,這兩個引數的取值為:

p = 1/4
MaxLevel = 32複製程式碼

skiplist的演算法效能分析

在這一部分,我們來簡單分析一下skiplist的時間複雜度和空間複雜度,以便對於skiplist的效能有一個直觀的瞭解。如果你不是特別偏執於演算法的效能分析,那麼可以暫時跳過這一小節的內容。

我們先來計算一下每個節點所包含的平均指標數目(概率期望)。節點包含的指標數目,相當於這個演算法在空間上的額外開銷(overhead),可以用來度量空間複雜度。

根據前面randomLevel()的偽碼,我們很容易看出,產生越高的節點層數,概率越低。定量的分析如下:

  • 節點層數至少為1。而大於1的節點層數,滿足一個概率分佈。
  • 節點層數恰好等於1的概率為1-p。
  • 節點層數大於等於2的概率為p,而節點層數恰好等於2的概率為p(1-p)。
  • 節點層數大於等於3的概率為p2,而節點層數恰好等於3的概率為p2(1-p)。
  • 節點層數大於等於4的概率為p3,而節點層數恰好等於4的概率為p3(1-p)。
  • ......

因此,一個節點的平均層數(也即包含的平均指標數目),計算如下:

Redis 為什麼用跳錶而不用平衡樹?

現在很容易計算出:

  • 當p=1/2時,每個節點所包含的平均指標數目為2;
  • 當p=1/4時,每個節點所包含的平均指標數目為1.33。這也是Redis裡的skiplist實現在空間上的開銷。

接下來,為了分析時間複雜度,我們計算一下skiplist的平均查詢長度。查詢長度指的是查詢路徑上跨越的跳數,而查詢過程中的比較次數就等於查詢長度加1。以前面圖中標出的查詢23的查詢路徑為例,從左上角的頭結點開始,一直到結點22,查詢長度為6。

為了計算查詢長度,這裡我們需要利用一點小技巧。我們注意到,每個節點插入的時候,它的層數是由隨機函式randomLevel()計算出來的,而且隨機的計算不依賴於其它節點,每次插入過程都是完全獨立的。所以,從統計上來說,一個skiplist結構的形成與節點的插入順序無關。

這樣的話,為了計算查詢長度,我們可以將查詢過程倒過來看,從右下方第1層上最後到達的那個節點開始,沿著查詢路徑向左向上回溯,類似於爬樓梯的過程。我們假設當回溯到某個節點的時候,它才被插入,這雖然相當於改變了節點的插入順序,但從統計上不影響整個skiplist的形成結構。

現在假設我們從一個層數為i的節點x出發,需要向左向上攀爬k層。這時我們有兩種可能:

  • 如果節點x有第(i+1)層指標,那麼我們需要向上走。這種情況概率為p。
  • 如果節點x沒有第(i+1)層指標,那麼我們需要向左走。這種情況概率為(1-p)。

這兩種情形如下圖所示:

Redis 為什麼用跳錶而不用平衡樹?

用C(k)表示向上攀爬k個層級所需要走過的平均查詢路徑長度(概率期望),那麼:

C(0)=0
C(k)=(1-p)×(上圖中情況b的查詢長度) + p×(上圖中情況c的查詢長度)複製程式碼

代入,得到一個差分方程並化簡:

C(k)=(1-p)(C(k)+1) + p(C(k-1)+1)
C(k)=1/p+C(k-1)
C(k)=k/p複製程式碼

這個結果的意思是,我們每爬升1個層級,需要在查詢路徑上走1/p步。而我們總共需要攀爬的層級數等於整個skiplist的總層數-1。

那麼接下來我們需要分析一下當skiplist中有n個節點的時候,它的總層數的概率均值是多少。這個問題直觀上比較好理解。根據節點的層數隨機演算法,容易得出:

  • 第1層連結串列固定有n個節點;
  • 第2層連結串列平均有n*p個節點;
  • 第3層連結串列平均有n*p2個節點;
  • ...

所以,從第1層到最高層,各層連結串列的平均節點數是一個指數遞減的等比數列。容易推算出,總層數的均值為log1/pn,而最高層的平均節點數為1/p。

綜上,粗略來計算的話,平均查詢長度約等於:

  • C(log1/pn-1)=(log1/pn-1)/p

即,平均時間複雜度為O(log n)。

當然,這裡的時間複雜度分析還是比較粗略的。比如,沿著查詢路徑向左向上回溯的時候,可能先到達左側頭結點,然後沿頭結點一路向上;還可能先到達最高層的節點,然後沿著最高層連結串列一路向左。但這些細節不影響平均時間複雜度的最後結果。另外,這裡給出的時間複雜度只是一個概率平均值,但實際上計算一個精細的概率分佈也是有可能的。詳情還請參見William Pugh的論文《Skip Lists: A Probabilistic Alternative to Balanced Trees》。

skiplist與平衡樹、雜湊表的比較

  • skiplist和各種平衡樹(如AVL、紅黑樹等)的元素是有序排列的,而雜湊表不是有序的。因此,在雜湊表上只能做單個key的查詢,不適宜做範圍查詢。所謂範圍查詢,指的是查詢那些大小在指定的兩個值之間的所有節點。
  • 在做範圍查詢的時候,平衡樹比skiplist操作要複雜。在平衡樹上,我們找到指定範圍的小值之後,還需要以中序遍歷的順序繼續尋找其它不超過大值的節點。如果不對平衡樹進行一定的改造,這裡的中序遍歷並不容易實現。而在skiplist上進行範圍查詢就非常簡單,只需要在找到小值之後,對第1層連結串列進行若干步的遍歷就可以實現。
  • 平衡樹的插入和刪除操作可能引發子樹的調整,邏輯複雜,而skiplist的插入和刪除只需要修改相鄰節點的指標,操作簡單又快速。
  • 從記憶體佔用上來說,skiplist比平衡樹更靈活一些。一般來說,平衡樹每個節點包含2個指標(分別指向左右子樹),而skiplist每個節點包含的指標數目平均為1/(1-p),具體取決於引數p的大小。如果像Redis裡的實現一樣,取p=1/4,那麼平均每個節點包含1.33個指標,比平衡樹更有優勢。
  • 查詢單個key,skiplist和平衡樹的時間複雜度都為O(log n),大體相當;而雜湊表在保持較低的雜湊值衝突概率的前提下,查詢時間複雜度接近O(1),效能更高一些。所以我們平常使用的各種Map或dictionary結構,大都是基於雜湊表實現的。
  • 從演算法實現難度上來比較,skiplist比平衡樹要簡單得多。

Redis中的skiplist實現

在這一部分,我們討論Redis中的skiplist實現。

在Redis中,skiplist被用於實現暴露給外部的一個資料結構:sorted set。準確地說,sorted set底層不僅僅使用了skiplist,還使用了ziplist和dict。這幾個資料結構的關係,我們下一章再討論。現在,我們先花點時間把sorted set的關鍵命令看一下。這些命令對於Redis裡skiplist的實現,有重要的影響。

sorted set的命令舉例

sorted set是一個有序的資料集合,對於像類似排行榜這樣的應用場景特別適合。

現在我們來看一個例子,用sorted set來儲存代數課(algebra)的成績表。原始資料如下:

  • Alice 87.5
  • Bob 89.0
  • Charles 65.5
  • David 78.0
  • Emily 93.5
  • Fred 87.5

這份資料給出了每位同學的名字和分數。下面我們將這份資料儲存到sorted set裡面去:

Redis 為什麼用跳錶而不用平衡樹?

對於上面的這些命令,我們需要的注意的地方包括:

  • 前面的6個zadd命令,將6位同學的名字和分數(score)都輸入到一個key值為algebra的sorted set裡面了。注意Alice和Fred的分數相同,都是87.5分。
  • zrevrank命令查詢Alice的排名(命令中的rev表示按照倒序排列,也就是從大到小),返回3。排在Alice前面的分別是Emily、Bob、Fred,而排名(rank)從0開始計數,所以Alice的排名是3。注意,其實Alice和Fred的分數相同,這種情況下sorted set會把分數相同的元素,按照字典順序來排列。按照倒序,Fred排在了Alice的前面。
  • zscore命令查詢了Charles對應的分數。
  • zrevrange命令查詢了從大到小排名為0~3的4位同學。
  • zrevrangebyscore命令查詢了分數在80.0和90.0之間的所有同學,並按分數從大到小排列。

總結一下,sorted set中的每個元素主要表現出3個屬性:

  • 資料本身(在前面的例子中我們把名字存成了資料)。
  • 每個資料對應一個分數(score)。
  • 根據分數大小和資料本身的字典排序,每個資料會產生一個排名(rank)。可以按正序或倒序。

Redis中skiplist實現的特殊性

我們簡單分析一下前面出現的幾個查詢命令:

  • zrevrank由資料查詢它對應的排名,這在前面介紹的skiplist中並不支援。
  • zscore由資料查詢它對應的分數,這也不是skiplist所支援的。
  • zrevrange根據一個排名範圍,查詢排名在這個範圍內的資料。這在前面介紹的skiplist中也不支援。
  • zrevrangebyscore根據分數區間查詢資料集合,是一個skiplist所支援的典型的範圍查詢(score相當於key)。

實際上,Redis中sorted set的實現是這樣的:

  • 當資料較少時,sorted set是由一個ziplist來實現的。
  • 當資料多的時候,sorted set是由一個dict + 一個skiplist來實現的。簡單來講,dict用來查詢資料到分數的對應關係,而skiplist用來根據分數查詢資料(可能是範圍查詢)。

這裡sorted set的構成我們在下一章還會再詳細地討論。現在我們集中精力來看一下sorted set與skiplist的關係,:

  • zscore的查詢,不是由skiplist來提供的,而是由那個dict來提供的。
  • 為了支援排名(rank),Redis裡對skiplist做了擴充套件,使得根據排名能夠快速查到資料,或者根據分數查到資料之後,也同時很容易獲得排名。而且,根據排名的查詢,時間複雜度也為O(log n)。
  • zrevrange的查詢,是根據排名查資料,由擴充套件後的skiplist來提供。
  • zrevrank是先在dict中由資料查到分數,再拿分數到skiplist中去查詢,查到後也同時獲得了排名。

前述的查詢過程,也暗示了各個操作的時間複雜度:

  • zscore只用查詢一個dict,所以時間複雜度為O(1)
  • zrevrank, zrevrange, zrevrangebyscore由於要查詢skiplist,所以zrevrank的時間複雜度為O(log n),而zrevrange, zrevrangebyscore的時間複雜度為O(log(n)+M),其中M是當前查詢返回的元素個數。

總結起來,Redis中的skiplist跟前面介紹的經典的skiplist相比,有如下不同:

  • 分數(score)允許重複,即skiplist的key允許重複。這在最開始介紹的經典skiplist中是不允許的。
  • 在比較時,不僅比較分數(相當於skiplist的key),還比較資料本身。在Redis的skiplist實現中,資料本身的內容唯一標識這份資料,而不是由key來唯一標識。另外,當多個元素分數相同的時候,還需要根據資料內容來進字典排序。
  • 第1層連結串列不是一個單向連結串列,而是一個雙向連結串列。這是為了方便以倒序方式獲取一個範圍內的元素。
  • 在skiplist中可以很方便地計算出每個元素的排名(rank)。

skiplist的資料結構定義

#define ZSKIPLIST_MAXLEVEL 32
#define ZSKIPLIST_P 0.25

typedef struct zskiplistNode {
    robj *obj;
    double score;
    struct zskiplistNode *backward;
    struct zskiplistLevel {
        struct zskiplistNode *forward;
        unsigned int span;
    } level[];
} zskiplistNode;

typedef struct zskiplist {
    struct zskiplistNode *header, *tail;
    unsigned long length;
    int level;
} zskiplist;複製程式碼

這段程式碼出自server.h,我們來簡要分析一下:

  • 開頭定義了兩個常量,ZSKIPLIST_MAXLEVEL和ZSKIPLIST_P,分別對應我們前面講到的skiplist的兩個引數:一個是MaxLevel,一個是p。
  • zskiplistNode定義了skiplist的節點結構。
    • obj欄位存放的是節點資料,它的型別是一個string robj。本來一個string robj可能存放的不是sds,而是long型,但zadd命令在將資料插入到skiplist裡面之前先進行了解碼,所以這裡的obj欄位裡儲存的一定是一個sds。有關robj的詳情可以參見系列文章的第三篇:《Redis內部資料結構詳解(3)——robj》。這樣做的目的應該是為了方便在查詢的時候對資料進行字典序的比較,而且,skiplist裡的資料部分是數字的可能性也比較小。
    • score欄位是資料對應的分數。
    • backward欄位是指向連結串列前一個節點的指標(前向指標)。節點只有1個前向指標,所以只有第1層連結串列是一個雙向連結串列。
    • level[]存放指向各層連結串列後一個節點的指標(後向指標)。每層對應1個後向指標,用forward欄位表示。另外,每個後向指標還對應了一個span值,它表示當前的指標跨越了多少個節點。span用於計算元素排名(rank),這正是前面我們提到的Redis對於skiplist所做的一個擴充套件。需要注意的是,level[]是一個柔性陣列(flexible array member),因此它佔用的記憶體不在zskiplistNode結構裡面,而需要插入節點的時候單獨為它分配。也正因為如此,skiplist的每個節點所包含的指標數目才是不固定的,我們前面分析過的結論——skiplist每個節點包含的指標數目平均為1/(1-p)——才能有意義。
  • zskiplist定義了真正的skiplist結構,它包含:
    • 頭指標header和尾指標tail。
    • 連結串列長度length,即連結串列包含的節點總數。注意,新建立的skiplist包含一個空的頭指標,這個頭指標不包含在length計數中。
    • level表示skiplist的總層數,即所有節點層數的最大值。

下圖以前面插入的代數課成績表為例,展示了Redis中一個skiplist的可能結構(點選看大圖):

Redis 為什麼用跳錶而不用平衡樹?

注意:圖中前向指標上面括號中的數字,表示對應的span的值。即當前指標跨越了多少個節點,這個計數不包括指標的起點節點,但包括指標的終點節點。

假設我們在這個skiplist中查詢score=89.0的元素(即Bob的成績資料),在查詢路徑中,我們會跨域圖中標紅的指標,這些指標上面的span值累加起來,就得到了Bob的排名(2+2+1)-1=4(減1是因為rank值以0起始)。需要注意這裡算的是從小到大的排名,而如果要算從大到小的排名,只需要用skiplist長度減去查詢路徑上的span累加值,即6-(2+2+1)=1。

可見,在查詢skiplist的過程中,通過累加span值的方式,我們就能很容易算出排名。相反,如果指定排名來查詢資料(類似zrange和zrevrange那樣),也可以不斷累加span並時刻保持累加值不超過指定的排名,通過這種方式就能得到一條O(log n)的查詢路徑。

Redis中的sorted set

我們前面提到過,Redis中的sorted set,是在skiplist, dict和ziplist基礎上構建起來的:

  • 當資料較少時,sorted set是由一個ziplist來實現的。
  • 當資料多的時候,sorted set是由一個叫zset的資料結構來實現的,這個zset包含一個dict + 一個skiplist。dict用來查詢資料到分數(score)的對應關係,而skiplist用來根據分數查詢資料(可能是範圍查詢)。

在這裡我們先來討論一下前一種情況——基於ziplist實現的sorted set。在本系列前面關於ziplist的文章裡,我們介紹過,ziplist就是由很多資料項組成的一大塊連續記憶體。由於sorted set的每一項元素都由資料和score組成,因此,當使用zadd命令插入一個(資料, score)對的時候,底層在相應的ziplist上就插入兩個資料項:資料在前,score在後。

ziplist的主要優點是節省記憶體,但它上面的查詢操作只能按順序查詢(可以正序也可以倒序)。因此,sorted set的各個查詢操作,就是在ziplist上從前向後(或從後向前)一步步查詢,每一步前進兩個資料項,跨域一個(資料, score)對。

隨著資料的插入,sorted set底層的這個ziplist就可能會轉成zset的實現(轉換過程詳見t_zset.c的zsetConvert)。那麼到底插入多少才會轉呢?

還記得本文開頭提到的兩個Redis配置嗎?

zset-max-ziplist-entries 128
zset-max-ziplist-value 64複製程式碼

這個配置的意思是說,在如下兩個條件之一滿足的時候,ziplist會轉成zset(具體的觸發條件參見t_zset.c中的zaddGenericCommand相關程式碼):

  • 當sorted set中的元素個數,即(資料, score)對的數目超過128的時候,也就是ziplist資料項超過256的時候。
  • 當sorted set中插入的任意一個資料的長度超過了64的時候。

最後,zset結構的程式碼定義如下:

typedef struct zset {
    dict *dict;
    zskiplist *zsl;
} zset;複製程式碼

Redis為什麼用skiplist而不用平衡樹?

在前面我們對於skiplist和平衡樹、雜湊表的比較中,其實已經不難看出Redis裡使用skiplist而不用平衡樹的原因了。現在我們看看,對於這個問題,Redis的作者 @antirez 是怎麼說的:

There are a few reasons:

1) They are not very memory intensive. It's up to you basically. Changing parameters about the probability of a node to have a given number of levels will make then less memory intensive than btrees.

2) A sorted set is often target of many ZRANGE or ZREVRANGE operations, that is, traversing the skip list as a linked list. With this operation the cache locality of skip lists is at least as good as with other kind of balanced trees.

3) They are simpler to implement, debug, and so forth. For instance thanks to the skip list simplicity I received a patch (already in Redis master) with augmented skip lists implementing ZRANK in O(log(N)). It required little changes to the code.

這段話原文出處:

news.ycombinator.com/item?id=117…

這裡從記憶體佔用、對範圍查詢的支援和實現難易程度這三方面總結的原因,我們在前面其實也都涉及到了。


系列下一篇我們將介紹intset,以及它與Redis對外暴露的資料型別set的關係,敬請期待。

(完)

其它精選文章

Redis 為什麼用跳錶而不用平衡樹?

相關文章