相信你們都使用過redis,對redis的數據結構也有所瞭解。那麼今天就從redis的數據結構的底層實現來研究下它爲啥如此高性能。redis
redis沒有直接用C語言傳統的字符串表示(以空字符串結尾的字符數組),而是本身建立了一種名爲簡單動態字符串的抽象類型(simple dynamic string,SDS)。算法
不過在redis裏面也會用到c字符串,可是它只做爲字符串字面量用在一些無須對字符串值進行修改的地方,好比日誌的打印。數據庫
當redis須要的是一個能夠修改的字符串值時,那麼就會用SDS來表示字符串值,好比redis的數據庫裏,包含字符串的鍵值對在底層都是由SDS實現的。數組
好比 客戶端執行安全
redis> set str "hello" OK
那麼redis將在數據庫建立一個新的鍵值對,其中 鍵值對的key是一個字符串對象,對象的底層實現是一個保存着 "str"的SDS。bash
鍵值對的value也是一個字符串對象,對象的底層實現也是一個保存着 "hello"的SDS。數據結構
又好比,客戶端執行命令:app
redis>rpush fruits "banana" "pair" "apple"
那麼redis將在數據庫建立一個新的鍵值對,其中函數
鍵值對的鍵是一個字符串對象,對象的底層實現是一個保存着 "fruits"的SDS。性能
鍵值對的value是一個列表對象,列表對象包含了都由sds實現的三個字符串對象。
除了用來保存數據庫中的字符串值之外,sds還被用來用做緩衝區等模塊。
如上圖所示:每一個sds.h/sdshdr結構表示一個SDS值:
struct sdshdr{ //記錄buf數組中已使用字節的數量 //等於SDS所保存字符串的長度 int len; //記錄buf數組中未使用的字節數量 int free; //字節數組,用於保存字符串 char buf[]; }
free = 0,表示這個sds沒有可分配的使用空間。
len = 5,表示保存了一個5字節長的字符串。
buf屬性是一個char類型的數組,數組前五個字節包存了'R','e','d','i','s'五個字符,而最後一個字節保存了空字符'\0'。
SDS遵循C字符串的空字符結尾的慣例的好處是,SDS能夠直接重用一部分C字符串函數庫裏面的函數。另外SDS裏面的空字符的1字節空間不計入len屬性中。
1.3 SDS與字符串的區別
(1).常熟複雜度O(1)獲取字符串長度
C字符串不記錄自身長度信息,因此爲了獲取C字符串的長度,程序必須遍歷整個字符串,因此這個操做的複雜度是O(N),
而SDS經過len屬性記錄了自身的字符串長度,因此複雜度是O(1)。
(2).杜絕緩衝區溢出
當SDS API須要對SDS進行修改時,API會先檢查SDS的空間是否知足要求,若是不知足,API將會自動將SDS的空間擴展至執行修改所需的大小,
而後纔去執行實際的修改操做。
而傳統的c字符串因爲不知道自身的長度,當修改字符串時可能會致使緩衝區溢出問題。
(3).減小修改字符串時帶來的內存重分配次數
先看看C字符串的數組進行一次內存重分配操做: -若是程序執行增常字符串操做,好比append操做,程序須要經過內存重分配來擴展底層數組的空間大小---若是忘了,會致使緩衝區溢出。 -若是程序猿執行縮短字符串操做,好比trim,若是忘記銅鼓內存重分配釋放再也不使用的空間,就會產生內存泄漏。 而SDS經過 空間預分配和惰性空間釋放不會有以上問題。 空間預分配就是 當對SDS進行修改之後,SDS的長度(len屬性值)將小於1MB,那麼程序分配和len屬性值一樣大小的未使用空間;若是SDS的長度(len屬性值)將>=1MB,那麼程序分配1MB大小的未使用空間 惰性空間釋放就是,SDS空間釋放後,只要修改free屬性的值就好
(4).二進制安全
其實就是文本信息含有多個空字符時,sds對其中的數據不作任何限制,數據寫入時怎麼樣,讀出來仍是怎麼樣。 而C字符串不行,以下圖所示,C字符串這隻能讀取到Redis
(5).兼容部分C字符串函數
redis列表鍵的底層實現之一就是鏈表。當一個鍵包含了數量較多的元素,又或者列表中包含的元素都是比較長的字符串時,redis就會使用鏈表做爲列表鍵的底層實現
除了鏈表鍵之外,發佈與訂閱,慢查詢,監視器等功能也用到了鏈表。
每一個鏈表節點使用一個adlist.h/listNode結構來表示
typedef struct listNode{ //前置節點 struct listNode *prev; //後置節點 struct listNode *next; //節點的值 void *value; }listNode;
多個listNode經過prev和next指針組成一個雙端鏈表,以下圖所示;
雖然僅僅使用多個listNode來組成鏈表,可是用adlist.h/list 來操做鏈表要方便不少。
typedef struct list{ //表頭節點 listNode *head; //表尾節點 listNode *tail; //鏈表所包含的節點數量 unsigned long len; //節點值複製函數 void *(*dup)(void *ptr); //節點值釋放函數 void *(*free)(void *ptr); //節點值對比函數 void *(*match)(void *ptr); }list;
list結構爲鏈表提供了表頭指針head,表尾指針tail,以及鏈表長度計數器len。另外還有dup,free,match這些函數。
redis的鏈表特性能夠總結以下: 1.雙端:鏈表節點帶有prev和next指針,獲取某個節點的前置節點和後置節點的複雜度都是O(1)。 2.無環:表頭節點的prev指針和表尾節點的next指針都指向null,對鏈表的訪問以null爲終點。 3.帶表頭指針和表尾指針:經過list結構的head指針和tail指針,程序獲取鏈表的表頭節點和表尾節點的複雜度是O(1)。 4.帶鏈表長度計數器:程序使用list結構的len屬性來對list持有的鏈表節點進行計數,程序獲取鏈表中節點數量的複雜度爲O(1)。 5.多態:鏈表節點使用void*指針來保存節點值,而且能夠經過list結構的dup,free,match三個屬性爲節點值設置類型特定函數,因此鏈表能夠用於保存各類不一樣類型的值。
字典,又稱爲符號表(symbol table),關聯數組(associative array)或者映射(map),是一個用於保存鍵值對的抽象數據結構。
redis數據庫就是使用字典來做爲底層實現的,對數據庫的增刪改查操做也是構建在對字典的操做之上的。字段仍是哈希鍵的底層實現之一,當一個哈希鍵包含的鍵值對比較多,又或者鍵值對中的元素比較長的時候,redis就會使用字典做爲哈希鍵的底層實現。
redis的字典使用哈希表做爲底層實現,一個哈希表裏面能夠有多個哈希表節點,而每一個哈希節點就保存了字典中的一個鍵值對。
typedef struct dictht{ //哈希表數組 dictEntry **table; //哈希表大小 unsigned long size; //哈希表大小掩碼,用於計算索引值 //老是等於size-1 unsigned long sizemask; //該哈希表已有節點數量 unsigned long used; }dictht;
table是一個數組,數組中每一個元素都是一個指向dict.h/dictEntry結構的指針,每一個dictEntry結構都保存着一個鍵值對。
哈希表節點使用dictEntry結構表示,每一個dictEntry結構都保存着一個鍵值對:
typedef struct dictEntry{ //鍵 void *key; //值 union{ void *val; uint64_tu64; int64_ts64; }v; struct dictEntry *next; }dictEntry;
key保存着鍵值對的鍵;
而v屬性則保存着鍵值對中的值,其中鍵值對的值能夠是一個指針,或者是一個uint64_t整數,又或者是一個int64_t整數;
next 屬性是指向另外一個哈希表節點的指針,這個指針能夠將多個哈希值相同的鍵值對鏈接在一塊兒,以此解決鍵衝突的問題。
typedef struct dict{ //類型特定函數 dictType *type; //私有數據 void *privdata; //哈希表 dictht ht[2]; //rehash索引 //當rehash不在進行時,值爲-1 int rehashidx; }dict;
type和privdata屬性是針對不一樣類型的鍵值對,爲建立多態字典而設置的;
ht屬性是包含兩個項的數組,每一個項都是一個dictht哈希表,通常狀況下,字典桌子使用ht[0],ht[1]哈希表侄仔進行rehash時使用。還有一個屬性rehashidx,記錄的是rehash目前的進度。
附圖看看就能理解了:
至於哈希算法和rehash操做將在下次附上。