Redis 爲何用跳錶而不用平衡樹?

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

Redis裏面使用skiplist是爲了實現sorted set這種對外的數據結構。sorted set提供的操做很是豐富,能夠知足很是多的應用場景。這也意味着,sorted set相對來講實現比較複雜。同時,skiplist這種數據結構對於不少人來講都比較陌生,由於大部分學校裏的算法課都沒有對這種數據結構進行過詳細的介紹。所以,爲了介紹得足夠清楚,本文會比這個系列的其它幾篇花費更多的篇幅。java

咱們將大致分紅三個部分進行介紹:node

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

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

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。實際上,它是在有序鏈表的基礎上發展起來的。less

咱們先來看一個有序鏈表,以下圖(最左側的灰色節點表示一個空的頭結點):

在這樣一個鏈表中,若是咱們要查找某個數據,那麼須要從頭開始逐個進行比較,直到找到包含數據的那個節點,或者找到第一個比給定數據大的節點爲止(沒找到)。也就是說,時間複雜度爲O(n)。一樣,當咱們要插入新數據的時候,也要經歷一樣的查找過程,從而肯定插入位置。

假如咱們每相鄰兩個節點增長一個指針,讓指針指向下下個節點,以下圖:

這樣全部新增長的指針連成了一個新的鏈表,但它包含的節點個數只有原來的一半(上圖中是7, 19, 26)。如今當咱們想查找數據的時候,能夠先沿着這個新鏈表進行查找。當碰到比待查數據大的節點時,再回到原來的鏈表中進行查找。好比,咱們想查找23,查找的路徑是沿着下圖中標紅的指針所指向的方向進行的:

  • 23首先和7比較,再和19比較,比它們都大,繼續向後比較。
  • 但23和26比較的時候,比26要小,所以回到下面的鏈表(原鏈表),與22比較。
  • 23比22要大,沿下面的指針繼續向後和26比較。23比26小,說明待查數據23在原鏈表中不存在,並且它的插入位置應該在22和26之間。

在這個查找過程當中,因爲新增長的指針,咱們再也不須要與鏈表中每一個節點逐個進行比較了。須要比較的節點數大概只有原來的一半。

利用一樣的方式,咱們能夠在上層新產生的鏈表上,繼續爲每相鄰的兩個節點增長一個指針,從而產生第三層鏈表。以下圖:

在這個新的三層鏈表結構上,若是咱們仍是查找23,那麼沿着最上層鏈表首先要比較的是19,發現23比19大,接下來咱們就知道只須要到19的後面去繼續查找,從而一會兒跳過了19前面的全部節點。能夠想象,當鏈表足夠長的時候,這種多層鏈表的查找方式能讓咱們跳過不少下層節點,大大加快查找的速度。

skiplist正是受這種多層鏈表的想法的啓發而設計出來的。實際上,按照上面生成鏈表的方式,上面每一層鏈表的節點個數,是下面一層的節點個數的一半,這樣查找過程就很是相似於一個二分查找,使得查找的時間複雜度能夠下降到O(log n)。可是,這種方法在插入數據的時候有很大的問題。新插入一個節點以後,就會打亂上下相鄰兩層鏈表上節點個數嚴格的2:1的對應關係。若是要維持這種對應關係,就必須把新插入的節點後面的全部節點(也包括新插入的節點)從新進行調整,這會讓時間複雜度從新蛻化成O(n)。刪除數據也有一樣的問題。

skiplist爲了不這一問題,它不要求上下相鄰兩層鏈表之間的節點個數有嚴格的對應關係,而是爲每一個節點隨機出一個層數(level)。好比,一個節點隨機出的層數是3,那麼就把它鏈入到第1層到第3層這三層鏈表中。爲了表達清楚,下圖展現瞭如何經過一步步的插入操做從而造成一個skiplist的過程(點擊看大圖):

從上面skiplist的建立和插入過程能夠看出,每個節點的層數(level)是隨機出來的,並且新插入一個節點不會影響其它節點的層數。所以,插入操做只須要修改插入節點先後的指針,而不須要對不少節點都進行調整。這就下降了插入操做的複雜度。實際上,這是skiplist的一個很重要的特性,這讓它在插入性能上明顯優於平衡樹的方案。這在後面咱們還會提到。

根據上圖中的skiplist結構,咱們很容易理解這種數據結構的名字的由來。skiplist,翻譯成中文,能夠翻譯成「跳錶」或「跳躍表」,指的就是除了最下面第1層鏈表以外,它會產生若干層稀疏的鏈表,這些鏈表裏面的指針故意跳過了一些節點(並且越高層的鏈表跳過的節點越多)。這就使得咱們在查找數據的時候可以先在高層的鏈表中進行查找,而後逐層下降,最終降到第1層鏈表來精確地肯定數據位置。在這個過程當中,咱們跳過了一些節點,從而也就加快了查找速度。

剛剛建立的這個skiplist總共包含4層鏈表,如今假設咱們在它裏面依然查找23,下圖給出了查找路徑:

須要注意的是,前面演示的各個節點的插入過程,實際上在插入以前也要先經歷一個相似的查找過程,在肯定插入位置後,再完成插入操做。

至此,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)。
  • ......

所以,一個節點的平均層數(也即包含的平均指針數目),計算以下:

如今很容易計算出:

  • 當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)。

這兩種情形以下圖所示:

用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裏面去:

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

  • 前面的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的可能結構(點擊看大圖):

注意:圖中前向指針上面括號中的數字,表示對應的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的關係,敬請期待。

(完)

其它精選文章

相關文章
相關標籤/搜索