本篇文章是基於做者黃建宏寫的書Redis設計與實現而作的筆記html
Redis中數據結構的底層實現包括如下對象:web
對象 |
解釋 |
簡單動態字符串 |
字符串的底層實現 |
鏈表 |
列表的底層實現 |
字典 |
運用在多個方面,包括Hash的實現等 |
跳躍表 |
有序集合的底層實現 |
整數集合 |
集合的底層實現之一 |
壓縮字典 |
列表鍵和哈希鍵的底層實現之一 |
Redis中並無直接使用C語言中的字符串,而是在其基礎之上實現了字符串的數據結構,叫作簡單動態字符串(SDS)。算法
其內部的定義爲:數組
/* Redis簡單動態字符串的數據結構 */
struct sdshdr {
//字符長度,記錄buf數組中已使用的字節數量
unsigned int len;
//當前可用空間,記錄buf數組中未使用的字節數量
unsigned int free;
//具體存放字符的buf
char buf[];
};
常數複雜度獲取字符串長度安全
由於SDS紀錄了自身字符串中已經使用的長度和未使用的長度,因此能夠在O(1)的時間複雜度內獲取到字符串長度,然而C字符串不得不經過遍歷整個字符串才能獲取到長度,其花費的則是O(N)。數據結構
杜絕緩衝區溢出app
和C字符串不一樣的是,SDS會利用紀錄下來的長度去檢查自身是否還有足夠的空間去容納新的需求,若是不知足的話,會先進行擴容,而後才執行新的操做。ide
減小修改字符串時帶來的內存重分配次數函數
C字符串中每次進行增長和縮短的操做時,都會涉及到內存的從新分配,SDS利用未使用空間來實現空間預分配和惰性空間釋放這兩種優化策略。性能
二進制安全
C字符串中判斷結束的條件是碰見空字符,不一樣的是,SDS則選擇了經過自身的len屬性的值來判斷字符串是否結束,這樣作的目的在於使得SDS不只僅可以存儲字符串,還能存儲二進制。
兼容部分C字符串函數
經過遵循C字符串以空字符結尾的慣例,SDS能夠在有須要的時候重用C語言中的string函數庫,好比對比函數,追加函數等等,從而實現代碼的重用。
鏈表提供了高效的結點重排能力,以及順序性的結點訪問方式,而且能夠經過增刪結點來靈活的調整鏈表的長度。
每一個鏈表結點使用的是一個listNode結構表示:
typedef struct listNode{
// 前置結點
struct listNode *prev;
// 後置結點
struct listNode * next;
// 結點值
void * value;
}
在此基礎之上,Redis經過封裝了listNode實現雙端鏈表,以下:
typedef struct list{
//表頭節點
listNode * head;
//表尾節點
listNode * tail;
//鏈表長度
unsigned long len;
//節點值複製函數
void *(*dup) (void *ptr);
//節點值釋放函數
void (*free) (void *ptr);
//節點值對比函數
int (*match)(void *ptr, void *key);
}
list結構爲鏈表提供了表頭指針head、表尾指針tail,以及鏈表長度計數器len,而dup、free和match成員則是用於實現多態鏈表所需的類型特定函數:
- dup函數用於
複製
鏈表結點所保存的值;
- free函數用於
釋放
鏈表結點所保存的值;
- match函數用於
對比
鏈表結點所保存的值和另外一個輸入值是否相等;
Redis的鏈表實現的特性總結以下:
- 雙端:鏈表節點帶有prev 和next 指針,獲取某個節點的前置節點和後置節點的時間複雜度都是O(1)。
- 無環:表頭節點的 prev 指針和表尾節點的next 都指向NULL,對鏈表的訪問時以NULL來作判斷是否截止。
- 帶表頭指針和表尾指針:由於鏈表帶有head指針和tail 指針,程序獲取鏈表頭結點和尾節點的時間複雜度爲O(1)。
- 帶鏈表長度計數器:鏈表中存有記錄鏈表長度的屬性 len。
- 多態:鏈表節點使用 void* 指針來保存節點值,而且能夠經過list 結構的dup 、 free、 match三個屬性爲節點值設置類型特定函數,因此鏈表能夠用來保存各類不一樣類型的值。
字典由哈希表組成,而哈希表又由哈希結點組成。
和鏈表同樣,Redis也本身實現了哈希表結點結構和哈希表結構,以下:
哈希表結點:
typeof struct dictEntry{
//鍵
void *key;
//值
union{
void *val;
uint64_tu64;
int64_ts64;
}
struct dictEntry *next;
}
每一個dictEntry結構都保存着一個鍵值對,分別對應屬性key和value,同時,next屬性是指向另一個哈希表結點的指針,做用就是將多個哈希值相同的哈希結點鏈接起來,以此來解決鍵衝突的問題。
Redis在dictEntry的基礎之上封裝實現了哈希表,以下:
typedef struct dictht {
//哈希表數組
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩碼,用於計算索引值
unsigned long sizemask;
//該哈希表已有節點的數量
unsigned long used;
}
其中須要提到的是sizemark,這個屬性和哈希值一塊兒決定一個鍵應該被放到table數組中的哪一個索引上面。
Redis在哈希表的基礎上封裝了dictht實現字典,以下:
typedef struct dict {
// 類型特定函數
dictType *type;
// 私有數據
void *privedata;
// 哈希表
dictht ht[2];
// rehash 索引
int rehashidx;
}
type屬性和privdata屬性是針對不一樣類型的鍵值對,爲建立多態字典而設置的。ht屬性是一個包含兩個項的數組,數組中的每一個項都是一個dictht哈希表,通常狀況下,字典只使用ht[0]哈希表,ht[1]哈希表只會在對ht[0]哈希表進行rehash時使用,而rehashidx則決定了rehash的進度,若是沒有進行rehash,其值則爲-1。
當要把一個新的鍵值對添加到字典裏面時,程序先要根據鍵值對中的鍵值計算出哈希值,再計算出索引值,而後將包含新鍵值對的哈希表結點放到哈希表數組的指定索引上面。
Redis使用murmurhash算法來計算哈希值
鍵衝突:存在兩個或者兩個以上的鍵被分配到了哈希表數組的同一個索引上面。
解決辦法:Redis的哈希表使用開鏈法來解決衝突,每一個哈希表結點都存在一個next指針,多個哈希表值能夠用next指針來構成一個單向鏈表,被分配到同一個索引上的多個結點能夠用這個單向鏈表鏈接起來,從而解決鍵衝突問題。
須要注意的是,由於考慮到下次方便再次讀取,所以老是將衝突的新結點插入到鏈表的表頭位置,也就是已有其餘結點的前面。
當哈希表的負載因子(已有數量/表數量)達到一個閥值之後,再次保存新的鍵值對時,衝突的概率將逐漸增長,所以須要進行響應的擴展(收縮)。
以擴展爲例,程序須要通過如下步驟(騰籠換鳥):
- 擴展空間:爲字典的ht[1]哈希表分配空間,其空間將會被擴展到第一個大於等於ht[0].used*2^n的整數值;
- 數據遷移:將保存在ht[0]上的全部鍵值對遷移到ht[1]中;
- 交換:遷移完後,釋放掉ht[0],將如今的ht[1]設置爲ht[0],而且爲ht[1]新建立一個空白哈希表,實現了相互交換的過程;
爲了不一次性交換所形成的性能影響,Redis採用的是漸進式rehash,也就是說,將會分屢次、漸進式的完成數據的遷移。因此會同時存在兩個哈希表數組,並不會急着一次性的將數ht[0]的數據遷移到ht[1]中,而是在每次操做的同時,將部分的ht[0]中的數據保存到ht[1]中,採用愚公移山的方式最終將ht[0]中的數據搬完,爲了不ht[0]中的數據不斷增長,相關的增長的操做都會做用在ht[1]之上,最後,搬完後的操做和以前的操做是一致的。
它的優勢在於:採用了分而治之的方式,將rehash鍵值對所需的操做均攤到字典的每一個添加、刪除、查找和更新操做之上,從而避免集中式rehash而帶來的龐大計算量。
我認爲它的缺點也是存在的,譬如在查詢的時候,可能在ht[0]中查找不到,還得跑到ht[1]中查找,無形中增長了開銷。
跳躍表是一種有序數據結構,它經過在每一個結點中維持多個指向其它結點的指針,從而達到快速訪問結點的目的。其查找的時間複雜度平都可以達到O(logn),最壞O(N),還能夠經過順序性操做來批量處理結點。
目前,Redis中只有兩個地方用到了跳躍表,一個是有序集合鍵,另一個是集羣結點中用做內部數據結構。
跳躍表由多個跳躍結點組成:
typedef struct zskiplistNode{
//層
struct zskiplistLevel{
//前進指針
struct zskiplistNode *forward;
//跨度
unsigned int span;
} level[];
//後退指針
struct zskiplistNode *backward;
//分值
double score;
//成員對象
robj *obj;
}
- 層:level 數組能夠包含多個元素,每一個元素都包含一個指向其餘節點的指針。
- 前進指針:用於指向表尾方向的前進指針
- 跨度:用於記錄兩個節點之間的距離
- 後退指針:用於從表尾向表頭方向訪問節點
- 分值和成員:跳躍表中的全部節點都按分值從小到大排序。成員對象指向一個字符串,這個字符串對象保存着一個SDS值
typedef struct zskiplist {
//表頭節點和表尾節點
structz skiplistNode *header,*tail;
//表中節點數量
unsigned long length;
//表中層數最大的節點的層數
int level;
}zskiplist;
其搜索的步驟爲,先經過頭結點定位到跳躍表結點,而後經過層去定位到下一個跳躍表結點的位置,直到找到給定分值的結點。
前提:當一個集合只包含整數值元素,而且這個集合的元素數量很少時。
typedef struct intset{
//編碼方式
uint32_t enconding;
// 集合包含的元素數量
uint32_t length;
//保存元素的數組
int8_t contents[];
}
由於可能存在存入的整數不符合已存在集合中的編碼格式,所以須要使用升級策略來解決。
- 擴展空間:根據新元素的類型,擴展整數集合底層數組的空間大小,併爲新元素分配空間
- 轉換編碼:將底層數組現有的全部元素都轉換成新的編碼格式,從新分配空間
- 添加:將新元素加入到底層數組中
一旦對數組進行了升級,編碼就會一直保存升級後的狀態。
壓縮列表是Redis爲了節約內存而開發的,是由一系列特殊編碼的連續內存塊組成的順序型數據結構。一個壓縮列表能夠包含任意多個結點,每一個結點能夠保存一個字節數或者一個整數值。