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框架