redis中跳錶的運用及原始碼解析(一)

熊景發表於2013-10-17

瞭解過Redis的都知道,Redis有一個非常有用的資料結構:SortSet,基於它,我們可以很輕鬆的實現一個Top N的應用。那麼,這個SortSet底層到怎麼樣實現的?怎麼樣實現才能既保證有序並提供插入,查詢的最優時間複雜度呢?這裡,便就是我將要給大家介紹的跳錶

跳錶的具體定義,請參考wikipedia跳錶定義,跳錶也是連結串列的一種,只不過它在連結串列的基礎上增加了跳躍功能,正是這個跳躍的功能,使得在查詢元素時,跳錶能夠提供O(log n)的時間複雜度。(通常,像紅黑樹等這樣的資料結構查詢的時間複雜度也是O(log n),但是正確實現一顆紅黑樹是比正確實現一個跳錶是要複雜很多很多的)

跳躍

那麼,這個跳躍的功能是怎麼實現的呢?為什麼能夠提供跟查詢樹一樣的O(log n)的時間複雜度呢?下邊我將藉助Redis中的程式碼來分析這些原理。(redis的完整程式碼,參看 redis

redis程式碼中,跳錶主要實現在 t_zset.c,跳錶節點的定義,則在redis.h中,我們首先來看跳錶節點的定義:

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

暫時先不關心robj *obj和unsigned int span這兩個屬性,robj是redis內部物件的定義, span是redis內部在計算節點的排名使用的。

在定義普通連結串列節點的時候(雙向連結串列)

struct ListNode 
{
    void *data;
    ListNode *prev;
    ListNode *next;
};  

prev為前向指標,next為後向指標。
那麼我們再來看redis中跳錶節點的前向節點指標的定義,

struct zskiplistNode *backward;  

backward指標表示其也是一個雙向連結串列,指向某個節點前向節點,這點跟普通連結串列的prev指標的含義是一樣的。
再看後向節點指標的定義:

struct zskiplistLevel {
        struct zskiplistNode *forward;
        unsigned int span;
} level[];  

很奇怪是不是?後向節點指標變成了一個陣列,這正是跳錶可以實現跳躍的實質,這個陣列中的forward元素不僅可以指向這個節點的直接後繼元素,也可以指向後繼元素的後繼元素,或者是後繼後繼後繼元素,依此類推。這也就是說,從當前的這個節點,可以以O(1)的時間複雜度跨過其直接後繼節點查詢其後的某個節點,這樣就實現了跳躍。(普通連結串列需要查詢某個節點的話,必須要進行遍歷操作,這對於有查詢效能要求的應用場景來說,是不能接受的!)

O(log n)

現在我們再來看看跳錶為什麼能提供O(log n)的時間複雜度。
一般,我們會在插入元素的時候,就保持跳錶的有序排列。請看下圖:
enter image description here

可以看到,正式這樣一種結構,我們每次在進行查詢操作的時候,都會從最高一層開始找,由於連結串列是有序的,所以期望的時間複雜度是O(log n),當然最壞情況下的時間複雜度是o(n),不過這種情況應該不太會遇到。

需要指出的是,實際的儲存結構,並不是像上圖這樣有這麼多節點,跟普通連結串列相比,跳錶額外的儲存空間只有那個前向節點陣列花費的,當然,這是一種以空間換取時間的策略。與它提供的特性相比,這點額外的空間我們是可以接受的。

redis跳錶實現解析

首先我們看下redis實現中幾個我們可以自己拿出來用的api:

/** 建立一個skiplist */ 
zskiplist *zslCreate(void)

/** 建立一個skiplist節點 */
zskiplistNode *zslCreateNode(int level, double score, robj *obj);

/** 釋放一個節點的記憶體 */
void zslFreeNode(zskiplistNode *node);

/** 釋放整個skiplist */
void zslFree(zskiplist *zsl);

/** 插入一個節點 */
zskiplistNode *zslInsert(zskiplist *zsl, double score, robj *obj)

/** 刪除一個節點,根據score刪除 */
int zslDelete(zskiplist *zsl, double score, robj *obj);

/** 這個函式是查詢某個節點對應的排名,其實就是在跳錶中位置 */
unsigned long zslGetRank(zskiplist *zsl, double score, robj *o);  

上邊這些使我們可以從redis原始碼中摳出來自己用的程式碼(當然還需要在封裝封裝),對於跳錶來說,重點需要關注的就是插入,查詢,刪除這幾個操作。

(下篇分析redis程式碼中skiplist插入,查詢,刪除的實現)

相關文章