redis是一個key-value存儲系統。和Memcached相似,它支持存儲的value類型相對更多,包括string(字符串)、list(鏈表)、set(集合)、zset(sorted set --有序集合)和hash(哈希類型)。這些數據類型都支持push/pop、add/remove及取交集並集和差集及更豐富的操做,並且這些操做都是原子性的。在此基礎上,redis支持各類不一樣方式的排序。與memcached同樣,爲了保證效率,數據都是緩存在內存中。區別的是redis會週期性的把更新的數據寫入磁盤或者把修改操做寫入追加的記錄文件,而且在此基礎上實現了master-slave(主從)同步。前端
1 內部數據結構redis
1.1 簡單動態字符串sds:算法
Sds (Simple Dynamic String,簡單動態字符串)是Redis 底層所使用的字符串表示,它被用在幾乎全部的Redis 模塊中。數據庫
1.1.1 sds的用途:api
a.實現字符串對象(StringObject):數據庫的鍵老是包含一個sds值,而數據庫的值保存的是String類型的時候值中包含sds值,不然包含的是long類型的值。數組
b.在redis程序內部用做char*類型的替代品:char*類型的功能單一抽象層次低不能支持redis的一些經常使用操做(長度計算和追加操做),緩存
1.1.2 redis中的字符串:安全
在C 語言中,字符串能夠用一個\0 結尾的char 數組來表示。好比說,hello world 在C 語言中就能夠表示爲"hello world\0" 。這種簡單的字符串表示在大多數狀況下都能知足要求,可是,它並不能高效地支持長度計算和追加(append)這兩種操做:
• 每次計算字符串長度(strlen(s))的複雜度爲θ(N) 。服務器
• 對字符串進行N 次追加,一定須要對字符串進行N 次內存重分配(realloc)。數據結構
在Redis 內部,字符串的追加和長度計算並很多見,而APPEND 和STRLEN 更是這兩種操做在Redis 命令中的直接映射,這兩個簡單的操做不該該成爲性能的瓶頸。Redis 除了處理C 字符串以外,還須要處理單純的字節數組,以及服務器協議等內容,因此爲了方便起見,Redis 的字符串表示還應該是二進制安全的:程序不該對字符串裏面保存的數據作任何假設,數據能夠是以\0 結尾的C 字符串,也能夠是單純的字節數組,或者其餘格式的數據。(這就是redis用sds替換char*的緣由:sds能夠高效的實現追加和長度計算,而且仍是二進制安全的)
typedef char *sds; struct sdshdr { // buf 已佔用長度 int len; // buf 剩餘可用長度 int free; // 實際保存字符串數據的地方 char buf[]; };
其實類型sds是char*的別名,而結構sdshdr則保存了len、free、bug這三個參數屬性。經過len 屬性,sdshdr 能夠實現複雜度爲θ(1) 的長度計算操做。另外一方面,經過對buf 分配一些額外的空間,並使用free 記錄未使用空間的大小,sdshdr 可讓執行追加操做所需的內存重分配次數大大減小
1.1.3 優化追加操做:
當執行追加操做時,好比如今給key=‘Hello World’的字符串後追加‘ again!’則這時的len=18,free由0變成了18,此時的buf='Hello World again!\0 ',也就是buf的內存空間是18+18+1=37個字節,其中‘\0’佔1個字節redis給字符串多分配了18個字節的預分配空間,因此下次還有append追加的時候,若是預分配空間足夠,就無須在進行空間分配了。在當前版本中,當新字符串的長度小於1M時,redis會分配他們所需大小一倍的空間,當大於1M的時候,就爲他們額外多分配1M的空間。
思考:這種分配策略會浪費內存資源嗎?
答:執行過APPEND 命令的字符串會帶有額外的預分配空間,這些預分配空間不會被釋放,除非該字符串所對應的鍵被刪除,或者等到關閉Redis 以後,再次啓動時從新載入的字符串對象將不會有預分配空間。由於執行APPEND 命令的字符串鍵數量一般並很少,佔用內存的體積一般也不大,因此這通常並不算什麼問題。另外一方面,若是執行APPEND 操做的鍵不少,而字符串的體積又很大的話,那可能就須要修改Redis 服務器,讓它定時釋放一些字符串鍵的預分配空間,從而更有效地使用內存。
1.1.4 小結:
a.redis的字符串表示爲sds,而不是C字符串(以\0結尾的char*)。
b.對比C字符串,sds有如下特性:高效的執行長度計算;高效的執行追加操做;二進制安全
c.sds會爲追加操做進行優化,加快追加操做的速度,並下降內存分配的次數,代價是多佔用了一些內存 ,並且這些內存不會被主動釋放。
1.2 雙向鏈表
鏈表做爲數組以外的一種經常使用序列抽象,是大多數高級語言的基本數據類型,由於C 語言自己不支持鏈表類型,大部分C 程序都會本身實現一種鏈表類型,Redis 也不例外——它實現了一個雙向鏈表結構。
1.2.1 雙向鏈表的應用:
雙向鏈表做爲一種通用的數據結構,在Redis 內部使用得很是多:它既是Redis 列表結構的底層實現之一,還被大量Redis 模塊所使用,用於構建Redis 的其餘功能。
注意:redis列表使用兩種數據結構做爲底層實現:雙向鏈表和壓縮列表。由於雙向鏈表佔用的內存比壓縮列表的要多,因此在建立新的列表鍵時,列表會優先考慮使用壓縮列表做爲底層實現,而且在有須要的時候,纔會從壓縮列表實現轉換到雙向鏈表實現。
除了實現列表類型之外,雙向列表還被不少redis內部模塊所應用:
a.事務模塊使用雙向鏈表來按順序保存輸入的命令;
b.服務器模塊使用雙向鏈表來保存多個客戶端;
c.訂閱/發送模塊使用雙向鏈表來保存訂閱模式的多個客戶端;
d.時間模塊使用雙向鏈表來保存時間事件(time event)
除此以外,其實相似的應用還有不少。
1.2.2 雙向鏈表的實現:
雙向鏈表是由listNode和list兩個數據結構組成,以下:
其中listNode是雙向鏈表的節點,包含prev(前驅指針)、next(後繼指針)和value(數值);list是雙向鏈表自己,包含head(表頭指針)、tail(表尾指針)、len(節點數量)、dup(複製函數)、free(釋放函數)和match(對比函數)
舉個例子:當刪除一個listNode時,若是包含這個節點的list的list->free函數不爲空,那麼刪除函數就會先調用list->free(listNode->value)清空節點的值,再執行餘下的刪除操做(好比說釋放節點)。
從結構上總結出他們的性能特徵:
a. listNode帶有prev和next兩個指針,所以對鏈表的遍歷能夠在兩個方向上進行:從表頭到表尾,或者從表尾到表頭。
b.list保存了head和tail兩個指針,所以對鏈表的表頭和表尾進行插入的複雜度都是θ(1)——這是實現LPUSH、RPOP、RPUSH、LPOP的關鍵。
c.list帶有保存九點數量的len屬性,因此計算鏈表長度的複雜度爲θ(1),因此LLEN的命令性能很高。
1.2.3 迭代器:
redis爲雙向鏈表實現了一個迭代器,這個迭代器能夠從兩個方向對雙向鏈表進行迭代:
*沿着節點的next指針前進,從表頭向表尾迭代;
*沿着節點的prev指針前進,從表尾向表頭迭代;
1.2.4 小結:
*redis實現了本身的雙向鏈表結構;
*雙向鏈表主要有兩個做用:
-做爲redis列表類型的底層實現之一;
-做爲通用數據結構,被其餘功能模塊所使用;
*雙向鏈表及其節點的性能特性以下:
-節點帶有前驅和後繼指針,訪問前驅節點和後繼節點的時間複雜度爲θ(1),而且堆鏈表的迭代能夠在從表頭到表尾和從表尾到表頭兩個方向進行;
-鏈表帶有隻想表頭和表尾的指針,所以對錶頭和表尾進行處理的複雜度爲θ(1);
-鏈表帶有記錄節點數量的屬性,因此能夠在θ(1)複雜度內返回鏈表的節點數量(長度);
1.3 字典
字典,又名映射(map)或關聯數組(associative array),他是一種抽象的數據結構,由一集鍵值對組成,各個鍵值對的鍵各不相同,程序能夠將新的鍵值對添加到字典中,或者基於鍵進行查找、更新或刪除操做。
1.3.1字典的應用
字典的主要用途有如下兩個:
1)實現數據庫鍵空間(key space)
redis是一個鍵值對數據庫,數據庫中的鍵值對就是由字典保存:每一個數據庫都有一個與之相對應的字典,這個字典被稱爲鍵空間(key space)。當用戶添加一個鍵值對到數據庫時(不論數據庫是什麼類型),程序就將該鍵值對添加到鍵空間;當用戶從數據庫刪除一個鍵值對時,程序就會將這個鍵值對從鍵空間刪除
2)用做Hash類型鍵的其中一種底層實現
redis的Hush類型鍵使用如下兩種數據結構做爲底層實現:字典、壓縮列表。由於壓縮列表比字典更節省內存,因此程序在建立新Hush鍵時,默認使用壓縮列表做爲底層實現,當有須要時,程序纔會將底層實現從壓縮列表轉換爲字典。
1.3.2 字典的實現
在衆多可能的實現中,redis選擇了高效且實現簡單的哈希表做爲字典的底層實現。
/* * 字典 ** 每一個字典使用兩個哈希表,用於實現漸進式rehash */ typedef struct dict { // 特定於類型的處理函數 dictType *type; // 類型處理函數的私有數據 void *privdata; // 哈希表(2 個) dictht ht[2]; // 記錄rehash 進度的標誌,值爲-1 表示rehash 未進行 int rehashidx; // 當前正在運做的安全迭代器數量 int iterators; } dict;
如下是用於處理dict 類型的API ,它們的做用及相應的算法複雜度:
注意:dict 類型使用了兩個指針分別指向兩個哈希表,其中0號哈希表(ht[0])表示字典主要使用的哈希表,而1號哈希表(ht[1])表示只有在程序對0號哈希表進行rehash時才使用。
/* * 哈希表 */ typedef struct dictht { // 哈希表節點指針數組(俗稱桶,bucket) dictEntry **table; // 指針數組的大小 unsigned long size; // 指針數組的長度掩碼,用於計算索引值 unsigned long sizemask; // 哈希表現有的節點數量 unsigned long used; } dictht;
table屬性是一個數組,數組的每一個元素都是一個指向dictEntry結構的指針。每一個dictEntry都保存着一個鍵值對,以及一個指向另外一個dictEntry結構的指針:
/* * 哈希表節點 */ typedef struct dictEntry { // 鍵 void *key; // 值 union { void *val; uint64_t u64; int64_t s64; } v; // 鏈日後繼節點 struct dictEntry *next; } dictEntry;
next屬性指向另外一個dictEntry結構,多個dictEntry能夠經過next指針串連成鏈表,dictht使用鏈地址法來處理鍵碰撞(鏈地址法:將所有具備一樣哈希地址的而不一樣keyword的數據元素鏈接到同一個單鏈表中。假設選定的哈希表長度爲m,則可將哈希表定義爲一個有m個頭指針組成的指針數組T[0..m-1]。凡是哈希地址爲i的數據元素,均以節點的形式插入到T[i]爲頭指針的單鏈表中。並且新的元素插入到鏈表的前端,這不只因爲方便。還因爲經常發生這種事實:新近插入的元素最有可能不久又被訪問。)哈希表例子:
1.3.3 添加鍵值對到字典
根據字典所處的狀態,將一個給定的鍵值對添加到字典可能會引發一系列複雜的操做:
*若是字典未初始化(字典的0號哈希表的table屬性爲空),那麼程序須要懟0號哈希表進行初始化;
*若是在插入時發生了鍵碰撞,那麼程序須要處理碰撞;
*若是新插入的元素使得字典知足了rehash條件,那麼須要啓動相應的rehash程序;
下面分別介紹添加操做在以上三種狀況下的執行:
(1)字典爲空
程序會根據dict.h/DICT_HT_INITIAL_SIZE 裏指定的大小爲d->ht[0]->table 分配空間(在目前的版本中,DICT_HT_INITIAL_SIZE 的值爲4 )。下面是添加鍵值對後的樣子:
(2)添加新鍵值對時發生碰撞處理
在哈希表實現中,當兩個不一樣的鍵擁有相同的哈希值時,咱們稱這兩個鍵發生碰撞,而哈希表實現必須對碰撞進行處理。通常會採用鏈地址法(使用鏈表將多個哈希值相同的節點串連在一塊兒),以下:
對於一個新的鍵值對key4 和value4 ,若是key4 的哈希值和key1 的哈希值相同,那麼它們將在哈希表的0 號索引上發生碰撞。經過將key4-value4 和key1-value1 兩個鍵值對用鏈表鏈接起來,就能夠解決碰撞的問題:
(3)觸發rehash操做:
對於使用鏈地址法來解決碰撞問題的哈希表dictht 來講,哈希表的性能依賴於它的大小(size屬性)和它所保存的節點的數量(used 屬性)之間的比率:
• 比率在1:1 時,哈希表的性能最好;
• 若是節點數量比哈希表的大小要大不少的話,那麼哈希表就會退化成多個鏈表,哈希表自己的性能優點就再也不存在;
以下:
對於上面這個哈希表,平均每次失敗查詢須要5個節點,效率極低。爲了在字典的鍵值對不斷增多的狀況下保持良好的性能,字典須要對所使用的哈希表(ht[0])進行rehash 操做:在不修改任何鍵值對的狀況下,對哈希表進行擴容,儘可能將比率維持在1:1左右。dictAdd 在每次向字典添加新鍵值對以前,都會對哈希表ht[0] 進行檢查,對於ht[0] 的size 和used 屬性,若是它們之間的比率ratio = used / size 知足如下任何一個條件的話,rehash 過程就會被激活:
1. 天然rehash :ratio >= 1 ,且變量dict_can_resize 爲真。
2. 強制rehash : ratio 大於變量dict_force_resize_ratio (目前版本中,dict_force_resize_ratio 的值爲5 )。
1.3.4 rehash執行過程:
1.建立一個比ht[0]->table 更大的ht[1]->table;
2.將ht[0]->table中的全部鍵值對前一代ht[1]->table;
3.將原有ht[0]的數據清空,並將ht[1]替換成新的ht[0];
下面具體介紹rehash的完整過程:
a.開始rehash
設置字典的rehashidx爲0,標誌着rehash的開始;爲ht[1]->table分配空間,大小至少是ht[0]->table 的兩倍;
b.rehash進行中
在這個階段,ht[0]->table 的節點會被逐漸遷移到ht[1]->table ,由於rehash 是分屢次進行的,字典的rehashidx 變量會記錄rehash 進行到ht[0] 的哪一個索引位置上:
c.rehash完畢:
在rehash 的最後階段,程序會執行如下工做:
1. 釋放ht[0] 的空間;
2. 用ht[1] 來代替ht[0] ,使原來的ht[1] 成爲新的ht[0] ;
3. 建立一個新的空哈希表,並將它設置爲ht[1] ;
4. 將字典的rehashidx 屬性設置爲-1 ,標識rehash 已中止;
如下是字典rehash 完畢以後的樣子:
對比字典rehash以前和以後,新的ht[0]空間更大,而且字典原有的鍵值對也沒有被修改或者刪除。
1.3.5 漸進式rehash:
在一個有不少鍵值對的字典裏,某個用戶在添加新鍵值對時觸發了rehash 過程,若是這個rehash 過程必須將全部鍵值對遷移完畢以後纔將結果返回給用戶,這樣的處理 方式將是很是不友好的。另外一方面,要求服務器必須阻塞直到rehash 完成,這對於Redis 服務器自己也是不能接受的。爲了解決這個問題,Redis 使用了漸進式(incremental)的rehash 方式:經過將rehash 分散到多個步驟中進行,從而避免了集中式的計算。
漸進式rehash 主要由_dictRehashStep 和dictRehashMilliseconds 兩個函數進行:
• _dictRehashStep 用於對數據庫字典、以及哈希鍵的字典進行被動rehash ;
• dictRehashMilliseconds 則由Redis 服務器常規任務程序(server cron job)執行,用於對數據庫字典進行主動rehash ;
_dictRehashStep:每次執行_dictRehashStep ,ht[0]->table 哈希表第一個不爲空的索引上的全部節點就會所有遷移到ht[1]->table 。在rehash 開始進行以後(d->rehashidx 不爲-1),每次執行一次添加、查找、刪除操做,_dictRehashStep 都會被執行一次。由於字典會保持哈希表大小和節點數的比率在一個很小的範圍內,因此每一個索引上的節點數量不會不少(從目前版本的rehash 條件來看,平均只有一個,最多一般也不會超過五個),因此在執行操做的同時,對單個索引上的節點進行遷移,幾乎不會對響應時間形成影響。
dictRehashMilliseconds: 能夠在指定的毫秒數內,對字典進行rehash 。當Redis 的服務器常規任務執行時,dictRehashMilliseconds 會被執行,在規定的時間內,儘量地對數據庫字典中那些須要rehash 的字典進行rehash ,從而加速數據庫字典的rehash進程。
在哈希表進行rehash 時,字典還會採起一些特別的措施,確保rehash 順利、正確地進行:
• 由於在rehash 時,字典會同時使用兩個哈希表,因此在這期間的全部查找、刪除等操做,除了在ht[0] 上進行,還須要在ht[1] 上進行。
• 在執行添加操做時,新的節點會直接添加到ht[1] 而不是ht[0] ,這樣保證ht[0] 的節點數量在整個rehash 過程當中都只減不增。
1.3.6 字典的收縮:
上面描述了經過rehash對字典的擴展,若是哈希表的也用節點數比已用節點數搭不少,那麼也能夠經過哈希表進行rehash來收縮字典。執行步驟以下:
1)建立一個比ht[0]->table 小的ht[1]->table ;
2)將ht[0]->table中的全部鍵值對遷移到ht[1]->table ;
3)將原有的ht[0]的數據清空,並將ht[1]替換成ht[0];
字典的收縮規則由htNeedsResize函數定義:
/* * 檢查字典的使用率是否低於系統容許的最小比率 ** 是的話返回1 ,不然返回0 。 */ int htNeedsResize(dict *dict) { long long size, used; // 哈希表已用節點數量 size = dictSlots(dict); // 哈希表大小 used = dictSize(dict); // 當哈希表的大小大於DICT_HT_INITIAL_SIZE // 而且字典的填充率低於REDIS_HT_MINFILL 時 // 返回1 return (size && used && size > DICT_HT_INITIAL_SIZE && (used*100/size < REDIS_HT_MINFILL)); }
在默認狀況下,REDIS_HT_MINFILL 的值爲10 ,也便是說,當字典的填充率低於10% 時,程序就能夠對這個字典進行收縮操做了。
字典收縮和字典擴展的一個區別是:
• 字典的擴展操做是自動觸發的(無論是自動擴展仍是強制擴展);
• 而字典的收縮操做則是由程序手動執行。
所以,使用字典的程序能夠決定什麼時候對字典進行收縮:
• 當字典用於實現哈希鍵的時候,每次從字典中刪除一個鍵值對,程序就會執行一次htNeedsResize 函數,若是字典達到了收縮的標準,程序將當即對字典進行收縮;
• 當字典用於實現數據庫鍵空間(key space) 的時候, 收縮的時機由redis.c/tryResizeHashTables 函數決定。
1.3.7 字典的迭代:
字典有本身的迭代器實現——對字典進行迭代實際上就是對字典所使用的哈希表進行迭代:
• 迭代器首先迭代字典的第一個哈希表,而後,若是rehash正在進行的話,就繼續對第二個哈希表進行迭代;
• 當迭代哈希表時,找到第一個不爲空的索引,而後迭代這個索引上的全部節點。
• 當這個索引迭代完了,繼續查找下一個不爲空的索引,如此循環,一直到整個哈希表都迭代完爲止。
字典的迭代器有兩種:
• 安全迭代器
• 不安全迭代器
/* * 字典迭代器 */ typedef struct dictIterator { dict *d; // 正在迭代的字典 int table, // 正在迭代的哈希表的號碼(0 或者1) index, // 正在迭代的哈希表數組的索引 safe; // 是否安全? dictEntry *entry, // 當前哈希節點 *nextEntry; // 當前哈希節點的後繼節點 } dictIterator;
如下是這個迭代器的api:
1.3.8 小結:
• 字典是由鍵值對構成的抽象數據結構;
• Redis 中的數據庫和哈希鍵都是基於字典來實現的;
• Redis 字典的底層實現爲哈希表,每一個字典使用兩個哈希表,通常狀況下只使用0號哈希表,只有在rehash進行時,纔會使用0號和1號哈希表;
• 哈希表使用鏈地址法來解決鍵衝突的問題;
• rehash能夠用於擴展和收縮哈希表;
• 對哈希表的rehash是分屢次、漸進式地進行。
1.4 跳躍表
跳躍表是一種隨機化的數據,這種數據結構以有序的方式在層次化的鏈表中保存元素,以下圖:
從上圖中咱們能夠看出跳躍表的結構組成:
• 表頭(head):負責維護跳躍表的節點指針。
• 跳躍表節點:保存着元素值,以及多個層。
• 層:保存着指向其餘元素的指針。高層的指針越過的元素數量大於等於低層的指針,爲了提升查找的效率,程序老是從高層先開始訪問,而後隨着元素值範圍的縮小,慢慢下降層次。
• 表尾:所有由NULL 組成,表示跳躍表的末尾。
1.4.1 跳躍表的實現:
a.容許重複的score值:多個不一樣的member的score值能夠相同;
b.進行對比操做時,不只要檢查score值,還要檢查member:當score值能夠重複時,單靠score值沒法判斷一個元素的身份,因此須要連member域都一併檢查才行;
c.每一個節點都帶有一個高度爲1層的後腿指針,用於從表頭方向向表尾方向迭代:當執行ZERVRANGE或ZREVRSNGEBYSCORE這類以逆序處理有序集的命令時,就會用到這個屬性。
typedef struct zskiplist { // 頭節點,尾節點 struct zskiplistNode *header, *tail; // 節點數量 unsigned long length; // 目前表內節點的最大層數 int level; } zskiplist; 跳躍表的節點由redis.h/zskiplistNode 定義: typedef struct zskiplistNode { // member 對象 robj *obj; // 分值 double score; // 後退指針 struct zskiplistNode *backward; // 層 struct zskiplistLevel { // 前進指針 struct zskiplistNode *forward; // 這個層跨越的節點數量 unsigned int span; } level[]; } zskiplistNode;
1.4.2 跳躍表的應用:
和字典、鏈表或者字符串這幾種在redis中大量使用的數據結構不一樣,跳躍表在redis的惟一做用就是實現有序集數據類型。
跳躍表將指向有序集的score值和member域的指針做爲元素,並以score值爲索引,對有序集元素進行排序。
1.4.3 小結:
跳躍表是一種隨機化數據結構,它的查找、添加、刪除操做均可以在對數指望時間下完成;
跳躍表目前在redis的惟一做用就是做爲有序集類型的底層數據結構(之一,另外一個構成有序集的結構是字典);
爲了適應自身的需求,redis基於William Pugh 論文中描述的跳躍表進行了修改,包括:
a.score值可重複;
b.對比一個元素須要同時檢查它的score值和member域;
c. 每一個節點帶有高度爲1層的後退指針,用於從表尾方向向表頭方向迭代。