Redis系列文章-資料結構篇

超人小冰發表於2020-11-15

Redis系列文章

前言:

工作原因,在學習mybatis知識後,2個月沒有補充新的知識了,最近拿起書本開始學習。打算寫下這個Redis系列的文章。

目錄結構如下:

  Redis內建資料結構

  Redis持久化

  Redis事件

  Redis節點複製功能

  Redis哨兵功能

  Redis叢集功能

  Redis排序功能實現

  Redis常見使用場景

Redis內建資料結構

說明: Redis資料庫裡每個鍵值對都是由物件構成。其中鍵總是字串物件,值可以為字串物件(string),列表物件(list),集合物件(set),有序集合物件(sortSet),Hash物件(hash)。那這些物件在Redis中所使用的底層資料結構是什麼,本章重點去闡述Redis內建資料結構。

簡單動態字串

Redis沒有直接使用C語言傳統的字串,而是自己構建了一種名為簡單動態字串的抽象型別。以下簡稱SDS。
舉個例子,客戶端執行以下命令:

 struct sdshdr {
 	// sds的長度
 	int len;
 	// buf[]中未使用空間
 	int free;
 	// 位元組陣列,用於儲存字串
 	char buf[];
 }

 SDS結構圖如下:

 

 

 

 

 SDS與普通C語言字串

C語言字串使用長度為N+1的字串陣列來表示長度為N的字串,並且字串最後一個元素總為 '/0';SDS在C字串基礎上加了free和len屬性,下文便分析SDS相較於C字串的優勢。

常數複雜度獲取字串長度

普通C字串獲取字元長度,需要從字元陣列遍歷到隊尾,時間複雜度為O(N)。SDS因為有len屬性,獲取字元長度只需讀取len的值就可,時間複雜度為O(1)。所以,客戶端使用STRLEN命令獲取字串的長度,不會對效能造成任何影響。

杜絕快取溢位 

C語言字串S1在修改字串值時,若沒有為S1分配足夠的空間,會造成快取溢位。
如S1,S2在記憶體中緊鄰著,S1儲存著"yes"字串,S2儲存著"no"字串。

 

 現將S1字串修改為"yessss",若此時程式猿之前沒有為S1分配足夠的空間,那木就會出現如下情況。

 

 s1的內容溢位到S2的位置了。但SDS不會出現這種情況,SDS在擴容時,會去檢查free的容量是否支撐此次擴容操作。若不支援,則會先進行記憶體分配,自動擴充到所需大小。

減少修改字串時帶來的記憶體重分配次數

正如上文說的,C語言每次對字串進行修改操作,都會涉及到記憶體重分配。但Redis作為資料庫,經常被用於資料頻繁修改,若每一次修改都需要進行記憶體重分配,會大大影響效能。而SDS通過引入free這一屬性,來解決這個問題。

空間預分配

當對SDS進行修改操作,新的字串長度n大於原來字串陣列長度,且小於1mb。那木在修改後,新的SDS的字元陣列長度為2*n。此時,SDS字串陣列中有len的長度未使用,則free = len,len = n; 舉個例子:

 

 現要將"hello"修改為"hello-world",此時SDS進行一次記憶體重分配。按照上面的規則,修改後的字串為11個位元組,那木會分配22位元組(此時不考慮後面的‘\0’),額外預留了11位元組。

 

 如果接下來,將"hello-world"修改為"hello-world11",則此時可以直接使用預留的空間,從而不用去重新記憶體分配。

惰性空間釋放

當將"hello-world"修改為"hello"時,Redis不會主動去釋放多餘的記憶體空間,將多餘的記憶體空間的大小寫到free屬性中。這樣做的原因是釋放記憶體空間也需要效能消耗,並且下次可能還會對字串進行擴容操作。儘管如此,Redis也提供了相應的API對惰性空間進行釋放。

連結串列

Redis沒有使用C語言內建的連結串列資料結構,構建了自己的連結串列實現。

struct listNode{
    // 前置節點
    struct listNode *prev;
    // 後置節點
    struct listNode *next;
    // 節點值
    void *value;
}listNode;

連結串列實現如下:

struct list{
    // 表頭節點
    listNode *head;
    // 表尾節點
    listNode *tail;
    // 節點數量
    long len;
    ......
}list;

可以看出,Redis內建的連結串列結構是一個包含連結串列長度,擁有雙端的雙向連結串列。因為雙向連結串列過於常見,所以總結如下:

1. 雙端: 擁有頭尾節點指標。獲取連結串列的表頭節點和表尾節點時間複雜度都為o(1)
2. 獲取連結串列長度複雜度: 擁有len屬性,獲取連結串列長度時間複雜度為o(1)

字典

字典作為一種資料結構,Redis構建了自己的字典實現。
舉個例子,執行如下命令:

redis> SET msg hello

在資料庫中建立一個鍵值對,這個鍵值對就是儲存在代表資料庫的字典裡面。除了資料庫外,字典也是Hash鍵(Redis對外提供的資料結構)的底層實現。
字典底層是用Hash表實現,資料結構如下:

struct dict{
    // 型別特定函式
    dictType *type;
    // 私有資料
    void *privdata;
    // 2張hash表
    dictht ht[2];
    ...
}dict;

重點關注hash表,是存放資料的資料結構,如下,是一個普通的字典,存了兩個鍵值對:

 

 此處解釋一下,字典中包含了兩個hash表,但字典只會使用其中一個ht[0],ht[1]只會在ht[0]進行rehash時使用。hash表結構簡單介紹下:

struct dictht{
    // 雜湊表陣列
    dictEntry **table;
    // 雜湊表大小
    long size;
    // 雜湊表大小掩碼,用於計算索引
    long sizemask;
    // 雜湊表節點數
    long used;
}dictht;

當發生hash衝突時,hash表使用鏈地址法解決。若在來一個鍵k3,計算索引,發現位於位置1,但此時位置1已有資料k1,則放置在k1後。

 

 跳躍表

跳躍表是很重要的資料結構,在大部分情況下,可以和平衡樹相媲美。Redis使用跳躍表作為有序集合鍵(對外提供的sortSet資料結構)的底層實現。
舉個例子,客戶端執行如下命令

redis 127.0.0.1:6379> ZADD skip 1 key1
(integer) 1
redis 127.0.0.1:6379> ZADD skip 2 key2
(integer) 1
redis 127.0.0.1:6379> ZADD skip 3 key3
(integer) 1

那木資料庫會生成一個跳躍表來存放上述的資料(skip 是字串鍵,所以不存在跳躍表裡)

 

 跳躍表結構如上圖,擁有頭尾節點,節點數量,節點的最高層數(除去頭節點),節點按從小到大排列。下文變分析其中的結構。

跳躍表節點

struct zskiplistNode{
    // 後退指標
    struct zskiplistNode *backward;
    // 分值
    double score;
    // 成員物件
    robj *obj;
    // 層結構
    struct zskiplistLevel{
           // 前進指標
        struct zskiplistNode *forward;
        // 跨度
        unsigned int span;
    } level[]
}zskiplistNode

每次存入一個帶有分值的值時,會建立一個跳躍表節點。建立跳躍表節點時,程式會根據冪次定律隨機生成一個1-32間的值作為level陣列的大小,這個大小就是層的高度。上圖分別展示了3個高度為4層,2層,5層的節點(頭節點32層,不存任何資料)

前進指標

每個層都有一個指向表尾方向的前進指標(level[i].forward),用於從表頭向表尾方向訪問節點。

上圖虛線表示訪問跳躍表所以節點的過程。

1. 先訪問表頭節點,然後從第四層的前進指標移動到表中的第二個節點。
2. 在第二個節點時,程式沿著第二層的前進指標移動到表中第三個節點。
3. 在第三個節點時,程式同樣沿著第二層的前進指標移動到表中第四個節點。
4. 當程式沿著第四個節點的前進指標訪問,碰到NULL,代表已經訪問結束,結束遍歷。

 跨度

層的跨度用於記錄兩個節點之間的距離。兩個節點之間跨度越大,他們就相距越遠。跨度是用來幹嘛的了?是用來計算排位的:在查詢某個節點的過程中,將沿途訪問過的所有層的跨度累加起來,得到的結果就是目標節點在跳躍表中的排位。
舉個例子,還是上圖,要找key3在跳躍表中的排位(即排在第幾位)那木在頭節點中的第5層前進指標直接指向key3,只經過一個層就可以得到了。跨度為3,則代表key3在跳躍表中的位數是第三位。

後退指標

節點可以通過後退指標反向遍歷跳躍表。但後退指標只能訪問它的前節點。這點與前進指標不同。

分值和成員

節點的分值是一個double型的浮點數。跳躍表所有節點都按照從小到大排列。在同一個跳躍表中,各個節點儲存的成員物件必須唯一,但分值可以想同。

結語

Redis內建了很多的資料結構,本文只是介紹了平常經常使用的型別。後續想把更多的篇幅留給Redis資料庫的實現。任重而道遠,還是想寫好這個系列。
如果對mybatis感興趣可以移步我的github,我以前部落格也對些許知識點進行了分析。覺得好的話麻煩點個star。一個純手寫的mybatis框架

相關文章