咱們都知道Redis是用C語言編寫的內存數據庫。可是因爲C幾乎沒有提供任何數據結構的封裝,因此Redis爲了實現更快,更安全的操做,本身在內部封裝了一系列的數據結構。 其中包括了簡單動態字符串、鏈表、字典、跳躍表、整數集合、壓縮列表,下面來一一介紹(畫的圖有點醜。。)。redis
在redis中,只有字符串字面量纔會用C字符串來表示(好比打印日誌),其它都使用SDS來表示(好比鍵值對的鍵都是用SDS表示的字符串)。算法
struct sdshdr {
// 記錄buf數組已使用的字節數,也就是SDS字符串的長度
int len;
// 記錄buf數組中未使用字節的數量
int free;
// 字節數組,用於保存字符串
char buf[];
}
複製代碼
SDS爲了能夠重用C字符串函數庫裏的函數,因此遵循了用空字符結尾,但這個空字符不計入len屬性中。數據庫
當一個列表鍵包含了數量比較多的元素,又或者列表中包含的元素都是比較長的字符串時,Redis就會使用鏈表做爲列表鍵的底層實現。同時,在發佈與訂閱、慢查詢、監視器等功能也用到了鏈表。數組
typedef struct list {
// 表頭節點
listNode *head;
// 表尾節點
listNode *tail;
// 鏈表所包含的節點數量
unsigned long len;
// 節點值複製函數
void *(*dup)(void *ptr);
// 節點值釋放函數
void (*free)(void *ptr);
// 節點值對比函數
void (*match)(void *ptr, void *key);
} list;
複製代碼
鏈表結構爲鏈表提供了表頭指針head、表尾指針tail,以及鏈表長度計數器len。而dup、free和match則是用於實現多臺所需的類型特定函數,從而實現能夠保存各類不一樣類型的值。安全
typedef struct listNode {
// 前置節點
struct listNode *prev;
// 後置節點
struct listNode *next;
// 節點的值
void *value;
}listNode;
複製代碼
多個listNode能夠經過prev和next指針組成雙端鏈表。可是無環,由於表頭節點的prev指針和表尾節點的next指針都指向NULL,因此對鏈表的訪問以NULL爲終點。 服務器
Redis的數據庫就是使用字典做爲底層來實現的。能夠把數據庫中全部的對象都看做是鍵值對,而這個鍵值對就是保存在表明數據庫的字典裏的。另外,哈希鍵的底層也是經過字典實現的。數據結構
typedef struct dict {
// 類型特定函數(我以爲這個應該是至關於Java中的泛型)
dictType *type;
// 私有數據
void *privdata;
// 哈希表數組,字典存儲使用ht[0],ht[1]在rehash遷移字典數據時使用
dictht ht[2];
// rehash索引,當rehash不在進行時,值爲-1
int trehashidx;
} dict;
複製代碼
type屬性和privdata屬性是針對不一樣類型的鍵值對,爲建立多態字典而設置的。函數
typedef struct dictht {
// 哈希表節點數組
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩碼,用於計算索引值,老是等於size - 1
unsigned long sizemask;
// 該哈希表已有節點的數量
unsigned long used;
} dictht;
複製代碼
sizemask屬性和哈希值一塊兒決定一個鍵應該被放到table數組的哪一個索引上面。性能
typedef struct dictEntry {
// 鍵
void *key;
// 值,用union結構存儲數據,用於壓縮空間
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 指向下個哈希表節點,造成鏈表(拉鍊法解決哈希衝突)
struct dictEntry *next;
} dictEntry;
複製代碼
當要將一個新的鍵值對添加到字典裏面時,程序須要先根據鍵值對的鍵計算出哈希值,再根據哈希表的sizemask和哈希值計算出索引值,而後再根據索引值,將包含新鍵值對的哈希表節點放到哈希表數組的指定索引上面。 Redis使用MurmurHash2算法來計算鍵的哈希值,這種算法的優勢在於,即便輸入的鍵是有規律的,算法仍能給出一個很好的隨機分佈性,而且算法的計算速度也很是快。優化
擴展和收縮哈希表的工做能夠經過執行rehash(從新散列)操做來完成,Redis對字典的哈希表執行rehash的步驟以下:
爲了不rehash對服務器性能形成影響,服務器並非一次性將ht[0]裏面的全部鍵值對所有rehash到ht[1],而是分屢次、漸進式的將ht[0]裏面的鍵值對慢慢的rehash到ht[1]。這裏就用到了rehashidx屬性,當程序處理rehash期間時,rehashidx值被設置爲0,當rehash操做完成時,又將它設置爲-1.
漸進式rehash的好處在於它採起分而治之的方式,將rehash鍵值對所需的計算工做均攤到對字典的每一個增刪改查操做上,從而避免了集中式rehash帶來的龐大計算量。
另外,在rehash期間,字典的刪除、查找、更新操做會在兩個哈希表上進行,若是在ht[0]沒有找到的話,就回去ht[1]找。而添加操做則所有在ht[1]進行,即全部新添加的鍵值對都會存到ht[1]裏面。
跳躍表是一種有序數據結構,它經過在每一個節點中維持多個指向其它節點的指針,從而達到快速訪問節點的目的。
跳躍表支持平均O(logN),最壞O(N)複雜度的節點查找,還能夠經過順序行操做來批量處理節點。在Redis中用跳躍表來做爲有序集合的底層實現之一。
typedef struct zskiplist {
// 表頭節點和表尾節點
struct zskiplistNode *header, *tail;
// 表中節點的數量
unsigned long length;
// 表中層數最大的節點的層數
int level;
} zskiplist;
複製代碼
level屬性用於在O(1)複雜度內獲取跳躍表中層數最高的那個節點的層數,注意,表頭節點的層高並不能算在裏面。
typedef struct zskiplistNode {
// 後退指針
struct zskiplistNode *backward;
// 分值
double score;
// 成員對象
robj *obj;
// 層
struct zskiplistLevel {
// 前進指針
struct zskiplistNode * forward;
// 跨度
unsigned int span;
} level[];
} zskiplistNode;
複製代碼
整數集合是集合鍵的底層實現之一,當一個集合只包含整數值元素,且元素數量很少時,將會使用整數集合做爲集合的底層實現。
typedef struct intset {
// 編碼方式
uint32_t encoding;
// 集合包含的元素數量
uint32_t length;
// 保存元素的數組
int8_t contents[];
} intset;
複製代碼
encoding的類型能夠是int16_t,int32_t或者int64_t。其中雖然contents被聲明爲int8_t,但實際上contents數組中不會保存int8_t類型的值,真正的類型仍是取決於encoding屬性的值。注意,若是contents數組中包含了不一樣整數類型的值,那麼encoding將被設置爲佔用空間最大的那個類型。同時,其餘值也將被升級編碼爲該類型。
當咱們要將一個新元素添加到整數集合裏時,而且新元素的類型比整數集合現有元素的類型都要長時,咱們將須要先將整數集合進行升級,才能將新元素添加進去。
升級整數集合並添加新元素分三步進行:
壓縮列表是列表建和哈希鍵的底層實現之一。當列表鍵或哈希鍵中的元素較少時,將會使用壓縮列表來做爲他們的底層實現。
壓縮列表是Redis爲了節約內存而開發的,是由一系列特殊編碼的連續內存塊組成的順序性數據結構。一個壓縮列表能夠包含任意多個節點,一個節點能夠保存一個SDS或一個整數值。
每一個壓縮列表節點能夠保存一個字節數組或者一個整數值。壓縮列表節點由三部分組成。
記錄了壓縮列表中前一個節點的長度。previous_entry_length屬性自身的長度能夠是1字節或5字節。
負責保存節點的值,節點值能夠是字節數組或整數,具體由encoding決定。
若是當前壓縮列表的節點長度都小於254字節,那麼用於記錄前一個字節長度的屬性previous_entry_length只須要用一個字節保存,可是如今要新加一個字節長度大於254字節的節點到壓縮列表中來,那麼將會形成連鎖更新,由於新加節點的後一個節點保存了這個節點的長度,須要將previous_entry_length擴展爲5字節的,而後繼續相似的擴展直到最後一個節點。
Redis的設計與實現 黃建宏 著