Redis系列文章-資料結構篇
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框架