Redis原始碼筆記 跳表

2022-10-10 08:39:12 字數 2691 閱讀 4152

redis 只有在 zset 物件的底層實現用到了跳表,跳表的優勢是能支援平均 o(logn) 複雜度的節點查詢。

zset 物件是唯一乙個同時使用了兩個資料結構來實現的 redis 物件,這兩個資料結構乙個是跳表,乙個是雜湊表。這樣的好處是既能進行高效的範圍查詢,也能進行高效單點查詢。

zset 物件能支援範圍查詢(如 zrangebyscore 操作),這是因為它的資料結構設計採用了跳表,而又能以常數複雜度獲取元素權重(如 zscore 操作),這是因為它同時採用了雜湊表進行索引。

鍊錶在查詢元素的時候,因為需要逐一查詢,所以查詢效率非常低,時間複雜度是o(n),於是就出現了跳表。跳表是在鍊錶基礎上改進過來的,實現了一種「多層」的有序鍊錶,這樣的好處是能快讀定位資料。

那跳表長什麼樣呢?下圖展示了乙個層級為 3 的跳表。

圖中頭節點有 l0~l2 三個頭指標,分別指向了不同層級的節點,然後每個層級的節點都通過指標連線起來:

如果我們要在鍊錶中查詢節點 4 這個元素,只能從頭開始遍歷鍊錶,需要查詢 4 次,而使用了跳表後,只需要查詢 2 次就能定位到節點 4,因為可以在頭節點直接從 l2 層級跳到節點 3,然後再往前遍歷找到節點 4。

可以看到,這個查詢過程就是在多個層級上跳來跳去,最後定位到元素。當資料量很大時,跳表的查詢複雜度就是 o(logn)。

zset 物件要同時儲存元素和元素的權重,對應到跳表節點結構裡就是 sds 型別的 ele 變數和 double 型別的 score 變數。每個跳表節點都有乙個後向指標,指向前乙個節點,目的是為了方便從跳表的尾節點開始訪問節點,這樣倒序查詢時很方便。

跳表是乙個帶有層級關係的鍊錶,而且每一層級可以包含多個節點,每乙個節點通過指標連線起來,實現這一特性就是靠跳表節點結構體中的zskiplistlevel 結構體型別的 level 陣列。

level 陣列中的每乙個元素代表跳表的一層,也就是由 zskiplistlevel 結構體表示,比如 leve[0] 就表示第一層,leve[1] 就表示第二層。zskiplistlevel 結構體裡定義了「指向下乙個跳表節點的指標」和「跨度」,跨度時用來記錄兩個節點之間的距離。

比如,下面這張圖,展示了各個節點的跨度。

第一眼看到跨度的時候,以為是遍歷操作有關,實際上並沒有任何關係,遍歷操作只需要用前向指標就可以完成了。

跨度實際上是為了計算這個節點在跳表中的排位。具體怎麼做的呢?因為跳表中的節點都是按序排列的,那麼計算某個節點排位的時候,從頭節點點到該結點的查詢路徑上,將沿途訪問過的所有層的跨度累加起來,得到的結果就是目標節點在跳表中的排位。

舉個例子,查詢圖中節點 3 在跳表中的排位,從頭節點開始查詢節點 3,查詢的過程只經過了乙個層(l3),並且層的跨度是 3,所以節點 3 在跳表中的排位是 3。

另外,圖中的頭節點其實也是 zskiplistnode 跳表節點,只不過頭節點的後向指標、權重、元素值都會被用到,所以圖中省略了這部分。

問題來了,由誰定義哪個跳表節點是頭節點呢?這就介紹「跳表」結構體了,如下所示:

跳表結構裡包含了:

跳表節點查詢過程

查詢乙個跳表節點的過程時,跳表會從頭節點的最高層開始,逐一遍歷每一層。在遍歷某一層的跳表節點時,會用跳表節點中的 sds 型別的元素和元素的權重來進行判斷,共有兩個判斷條件:

如果上面兩個條件都不滿足,或者下乙個節點為空時,跳表就會使用目前遍歷到的節點的 level 陣列裡的下一層指標,然後沿著下一層指標繼續查詢,這就相當於跳到了下一層接著查詢。

如果要查詢「元素:abcd,權重:4」的節點,查詢的過程是這樣的:

「元素:abc,權重:3」節點的 leve[1] 的下乙個指標指向了「元素:abcde,權重:4」的節點,然後將其和要查詢的節點比較。雖然「元素:abcde,權重:4」的節點的權重和要查詢的權重相同,但是當前節點的 sds 型別資料「大於」要查詢的資料,所以會繼續跳到「元素:abc,權重:3」節點的下一層去找,也就是 leve[0];

跳表的相鄰兩層的節點數量最理想的比例是 2:1,查詢複雜度可以降低到 o(logn)

如果採用新增節點或者刪除節點時,來調整跳表節點以維持比例的方法的話,會帶來額外的開銷。

redis 則採用一種巧妙的方法是,跳表在建立節點的時候,隨機生成每個節點的層數,並沒有嚴格維持相鄰兩層的節點數量比例為 2 : 1 的情況。

具體的做法是,跳表在建立節點時候,會生成範圍為[0-1]的乙個隨機數,如果這個隨機數小於 0.25(相當於概率 25%),那麼層數就增加 1 層,然後繼續生成下乙個隨機數,直到隨機數的結果大於 0.25 結束,最終確定該節點的層數。

這樣的做法,相當於每增加一層的概率不超過 25%,層數越高,概率越低,層高最大限制是 64。

redis原始碼閱讀筆記

在redis中乙個資料庫結構體是這樣的 每個dict是乙個hash表 typedef struct redisdb redisdb dict欄位中存放以key值為鍵,以value指標為值的hash表項dict根據型別的不同分為如下幾種 1 字串 string 操作 set key value get...

redis原始碼學習筆記

目錄 1 從資料結構開始 圖為原始碼,附帶個人簡單分析 a 動態字串 檔案 sds.h sds.c 前言 s sizeof struct sdshdr 的解釋為buf為柔性陣列,不占用空間,僅僅為偏移量,所以s指標向後退乙個結構體大小為結構體位址所在。分析 這個結構是整個動態字串的基礎,sds為 s...

redis原始碼筆記 ae epoll c

這部分 是具體事件觸發網路庫的底層實現。linux下有epoll設施,而且其效率是現在最高的。注意即使高效如redis,其也只是選擇了自動檔 水平觸發 自動檔和手動檔的典故請自行google 據說libevent也是使用的水平觸發。廢話不多說,看 吧。1 include 2 3 typedef st...