redis的字符串不是直接用c語言的字符串,而是用了一種稱爲簡單動態字符串(SDS)的抽象類型,並將其做爲默認字符串。node
redis中包含字符串值的鍵值對在底層都是由SDS實現的。redis
1 /* 2 * 保存字符串對象的結構 3 */ 4 struct sdshdr { 5 6 // buf 中已佔用空間的長度 7 int len; 8 9 // buf 中剩餘可用空間的長度 10 int free; 11 12 // 數據空間 13 char buf[]; 14 };
SDS遵循C字符串以空字符結尾的慣例,可是那1個字節不計算在len中。算法
能夠重用C字符串庫函數裏的函數。數據庫
一、常數複雜度獲取字符串長度數組
C語言若是要獲取字符串的長度,須要從第一個字符開始,遍歷整個字符串,直到遍歷到\0符號,時間複雜度是O(N),即字符串的長度。緩存
而redis因爲已經存儲了字符串的長度,所以,時間複雜度是O(1)。安全
這樣,避免了獲取大字符串長度時時間的緩慢。服務器
二、杜絕緩衝區溢出數據結構
C語言給字符串開闢一個存儲空間,若是對此存儲空間的使用超過開闢的空間,會致使內存溢出。app
例如使用字符串拼接等方式時,就很容易出現此問題。而若是每次拼接以前都要計算每一個字符串的長度,時間上又要耗費好久。
redis的SDS中內置一個sdscat函數,也是用於字符串的拼接。可是在執行操做以前,其會先檢查空間是否足夠。
若是free的值不夠,會再申請內存空間,避免溢出。
三、減小內存分配次數
C語言的字符串長度和底層數組之間存在關聯,所以字符串長度增長時須要再分配存儲空間,避免溢出;字符串長度減小時,須要釋放存儲空間,避免內存泄漏。
redis的sds,主要是經過free字段,來進行判斷。經過未使用空間大小,實現了空間預分配和惰性空間釋放。
1)空間預分配
當須要增加字符串時,sds不只會分配足夠的空間用於增加,還會預分配未使用空間。
分配的規則是,若是增加字符串後,新的字符串比1MB小,則額外申請字符串當前所佔空間的大小做爲free值;若是增加後,字符串長度超過1MB,則額外申請1MB大小。
上述機制,避免了redis字符串增加狀況下頻繁申請空間的狀況。每次字符串增加以前,sds會先檢查空間是否足夠,若是足夠則直接使用預分配的空間,不然按照上述機制申請使用空間。
1 /* 2 * 對 sds 中 buf 的長度進行擴展,確保在函數執行以後, 3 * buf 至少會有 addlen + 1 長度的空餘空間 4 * (額外的 1 字節是爲 \0 準備的) 5 * 6 * 返回值 7 * sds :擴展成功返回擴展後的 sds 8 * 擴展失敗返回 NULL 9 * 10 * 複雜度 11 * T = O(N) 12 */ 13 sds sdsMakeRoomFor(sds s, size_t addlen) { 14 15 struct sdshdr *sh, *newsh; 16 17 // 獲取 s 目前的空餘空間長度 18 size_t free = sdsavail(s); 19 20 size_t len, newlen; 21 22 // s 目前的空餘空間已經足夠,無須再進行擴展,直接返回 23 if (free >= addlen) return s; 24 25 // 獲取 s 目前已佔用空間的長度 26 len = sdslen(s); 27 sh = (void*) (s-(sizeof(struct sdshdr))); 28 29 // s 最少須要的長度 30 newlen = (len+addlen); 31 32 // 根據新長度,爲 s 分配新空間所需的大小 33 if (newlen < SDS_MAX_PREALLOC) 34 // 若是新長度小於 SDS_MAX_PREALLOC 默認1M 35 // 那麼爲它分配兩倍於所需長度的空間 36 newlen *= 2; 37 else 38 // 不然,分配長度爲目前長度加上 SDS_MAX_PREALLOC 39 newlen += SDS_MAX_PREALLOC; 40 // T = O(N) 41 newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1); 42 43 // 內存不足,分配失敗,返回 44 if (newsh == NULL) return NULL; 45 46 // 更新 sds 的空餘長度 47 newsh->free = newlen - len; 48 49 // 返回 sds 50 return newsh->buf; 51 }
2)懶惰空間釋放
懶惰空間釋放用於優化sds字符串縮短的操做
當須要縮短sds的長度時,並不當即釋放空間,而是使用free來保存剩餘可用長度,並等待未來使用。
當有剩餘空間,而有有增加字符串操做時,則又會調用空間預分配機制。
當redis內存空間不足時,會自動釋放sds中未使用的空間,所以也不須要擔憂內存泄漏問題。
四、二進制安全
SDS 的 API 都是二進制安全的: 全部 SDS API 都會以處理二進制的方式來處理 SDS 存放在 buf 數組裏的數據, 程序不會對其中的數據作任何限制、過濾、或者假設 —— 數據在寫入時是什麼樣的, 它被讀取時就是什麼樣。
sds考慮字符串長度,是經過len屬性,而不是經過\0來判斷。
五、兼容部分C語言字符串函數
redis兼容c語言對於字符串末尾採用\0進行處理,這樣使得其能夠複用部分c語言字符串函數的代碼,實現代碼的精簡性。
列表鍵的底層之一是鏈表。(底層也有多是壓縮列表)
當列表鍵包含了許多元素,或者元素是比較長的字符串的時候,就會用到鏈表做爲列表鍵的底層實現。
一、節點結構
1 /* 2 * 雙端鏈表節點 3 */ 4 typedef struct listNode { 5 6 // 前置節點 7 struct listNode *prev; 8 9 // 後置節點 10 struct listNode *next; 11 12 // 節點的值 13 void *value; 14 15 } listNode;
其中prev指向前一個節點,next指向後一個節點,value存儲着節點自己的值。多個listNode組成雙向鏈表,以下圖所示:
二、鏈表結構
1 /* 2 * 雙端鏈表結構 3 */ 4 typedef struct list { 5 6 // 表頭節點 7 listNode *head; 8 9 // 表尾節點 10 listNode *tail; 11 12 // 節點值複製函數 13 void *(*dup)(void *ptr); 14 15 // 節點值釋放函數 16 void (*free)(void *ptr); 17 18 // 節點值對比函數 19 int (*match)(void *ptr, void *key); 20 21 // 鏈表所包含的節點數量 22 unsigned long len; 23 24 } list;
鏈表以下圖所示:
redis的鏈表特性以下:
1)雙向:每一個listNode節點帶有prev和next指針,能夠找到前一個節點和後一個節點,具備雙向性。
2)無環:list鏈表的head節點的prev和tail節點的next指針都是指向null。
3)帶表頭指針和尾指針:即上述的head和tail,獲取頭指針和尾指針的時間複雜度O(1)。
4)帶鏈表長度計數器;即list的len屬性,記錄節點個數,所以獲取節點個數的時間複雜度O(1)。
5)多態:鏈表使用void*指針來保存節點的值,能夠經過list的dup、free、match三個屬性爲節點值設置類型特定函數,因此鏈表能夠用於保存不一樣類型的值。
字典,又稱符號表、關聯數組、映射,是一種保存鍵值對的抽象數據結構。
每一個鍵(key)和惟一的值(value)關聯,鍵是獨一無二的,經過對鍵的操做能夠對值進行增刪改查。
redis中字典應用普遍,對redis數據庫的增刪改查就是經過字典實現的。即redis數據庫的存儲,和大部分關係型數據庫不一樣,不採用B+tree進行處理,而是採用hash的方式進行處理。
字典仍是hash鍵的底層實現之一。
當hash鍵包含了許多元素,或者元素是比較長的字符串的時候,就會用到字典做爲hash鍵的底層實現。
redis的字典,底層是使用哈希表實現,每一個哈希表有多個哈希節點,每一個哈希節點保存了一個鍵值對。
一、哈希表
1 /* 2 * 哈希表 3 * 4 * 每一個字典都使用兩個哈希表,從而實現漸進式 rehash 。 5 */ 6 typedef struct dictht { 7 8 // 哈希表數組 9 dictEntry **table; 10 11 // 哈希表大小 12 unsigned long size; 13 14 // 哈希表大小掩碼,用於計算索引值 15 // 老是等於 size - 1 16 unsigned long sizemask; 17 18 // 該哈希表已有節點的數量 19 unsigned long used; 20 21 } dictht;
其中,table是一個數組,裏面的每一個元素指向dictEntry(哈希表節點)結構的指針,dictEntry結構是鍵值對的結構;
size表示哈希表的大小,也是table數組的大小;
used表示table目前已有的鍵值對節點數量;
sizemask一直等於size-1,該值與哈希值一塊兒決定一個屬性應該放到table的哪一個位置。
大小爲4的空哈希表結構以下圖(左邊一列的圖)所示:
二、哈希表節點
1 /* 2 * 哈希表節點 3 */ 4 typedef struct dictEntry { 5 6 // 鍵 7 void *key; 8 9 // 值 10 union { 11 void *val; 12 uint64_t u64; 13 int64_t s64; 14 } v; 15 16 // 指向下個哈希表節點,造成鏈表 17 struct dictEntry *next; 18 19 } dictEntry;
其中,key表示節點的鍵;union表示key對應的值,能夠是指針、uint64_t整數或int64_t整數;
next是指向另外一個哈希表節點的指針,該指針將多個哈希值相同的鍵值對鏈接在一塊兒,避免由於哈希值相同致使的衝突。
哈希表節點以下圖(左邊第一列是哈希表結構,表節點結構從左邊第二列開始)所示:
三、字典
1 /* 2 * 字典 3 */ 4 typedef struct dict { 5 6 // 類型特定函數 7 dictType *type; 8 9 // 私有數據 10 void *privdata; 11 12 // 哈希表 13 dictht ht[2]; 14 15 // rehash 索引 16 // 當 rehash 不在進行時,值爲 -1 17 int rehashidx; /* rehashing not in progress if rehashidx == -1 */ 18 19 // 目前正在運行的安全迭代器的數量 20 int iterators; /* number of iterators currently running */ 21 22 } dict;
type用於存放用於處理特定類型的處理函數;
privdata用於存放私有數據,保存傳給type內的函數的數據;
rehash是一個索引,當沒有在rehash進行時,值是-1;
ht是包含兩個項的數組,每一個項是一個哈希表,通常狀況下只是用ht[0],只有在對ht[0]進行rehash時,纔會使用ht[1]。
完整的字典結構以下圖所示:
要將新的鍵值對加到字典,程序要先對鍵進行哈希算法,算出哈希值和索引值,再根據索引值,把包含新鍵值對的哈希表節點放到哈希表數組指定的索引上。
redis實現哈希的代碼是:
hash =dict->type->hashFunction(key); index = hash& dict->ht[x].sizemask;
算出來的結果中,index的值是多少,則key會落在table裏面的第index個位置(第一個位置index是0)。
其中,redis的hashFunction,採用的是murmurhash2算法,是一種非加密型hash算法,其具備高速的特色。
當兩個或者以上的鍵被分配到哈希表數組的同一個索引上,則稱這些鍵發生了衝突。
爲了解決此問題,redis採用鏈地址法。被分配到同一個索引上的多個節點能夠用單鏈錶鏈接起來。
由於沒有指向尾節點的指針,因此老是將新節點加在表頭的位置。(O(1)時間)
隨着操做進行,哈希表保存的鍵值對會增長或減小,爲了讓哈希表的負載因子(load factor)維持在一個合理範圍,當一個哈希表保存的鍵太多或者太少,須要對哈希表進行擴展或者收縮。擴展或收縮哈希表的過程,就稱爲rehash。
rehash步驟以下:
一、給字典的ht[1]申請存儲空間,大小取決於要進行的操做,以及ht[0]當前鍵值對的數量(ht[0].used)。假設當前ht[0].used=x。
若是是擴展,則ht[1]的值是第一個大於等於x*2的2n的值。例如x是30,則ht[1]的大小是第一個大於等於30*2的2n的值,即64。
若是是收縮,則ht[1]的值是第一個大於等於x的2n的值。例如x是30,則ht[1]的大小是第一個大於等於30的2n的值,即32。
二、將保存在ht[0]上面的全部鍵值對,rehash到ht[1],即對每一個鍵從新採用哈希算法的方式計算哈希值和索引值,再放到相應的ht[1]的表格指定位置。
三、當ht[0]的全部鍵值對都rehash到ht[1]後,釋放ht[0],並將ht[1]設置爲ht[0],再新建一個空的ht[1],用於下一次rehash。
rehash條件:
負載因子(load factor)計算:
load_factor =ht[0].used / ht[0].size,即負載因子大小等於當前哈希表的鍵值對數量,除以當前哈希表的大小。
擴展:
當如下任一條件知足,哈希表會自動進行擴展操做:
1)服務器目前沒有在執行BGSAVE或者BGREWRITEAOF命令,且負載因子大於等於1。
2)服務器目前正在在執行BGSAVE或者BGREWRITEAOF命令,且負載因子大於等於5。
收縮:
當負載因子小於0.1時,redis自動開始哈希表的收縮工做。
redis對ht[0]擴展或收縮到ht[1]的過程,並非一次性完成的,而是漸進式、分屢次的完成,以免若是哈希表中存有大量鍵值對,一次性複製過程當中,佔用資源較多,會致使redis服務停用的問題。
漸進式rehash過程以下:
一、爲ht[1]分配空間,讓字典同時持有ht[0]和ht[1]兩張哈希表。
二、將字典中的rehashidx設置成0,表示正在rehash。rehashidx的值默認是-1,表示沒有在rehash。
三、在rehash進行期間,程序處理正常對字典進行增刪改查之外,還會順帶將ht[0]哈希表上,rehashidx索引上,全部的鍵值對數據rehash到ht[1],而且rehashidx的值加1。
四、當某個時間節點,所有的ht[0]都遷移到ht[1]後,rehashidx的值從新設定爲-1,表示rehash完成。
漸進式rehash採用分而治之的工做方式,將哈希表的遷移工做所耗費的時間,平攤到增刪改查中,避免集中rehash致使的龐大計算量。
在rehash期間,對哈希表的查找、修改、刪除,會先在ht[0]進行。
若是ht[0]中沒找到相應的內容,則會去ht[1]查找,並進行相關的修改、刪除操做。而增長的操做,會直接增長到ht[1]中,目的是讓ht[0]只減不增,加快遷移的速度。
字典在redis中普遍應用,包括數據庫和hash數據結構。
每一個字典有兩個哈希表,一個是正常使用,一個用於rehash期間使用。
當redis計算哈希時,採用的是MurmurHash2哈希算法。
哈希表採用鏈地址法避免鍵的衝突,被分配到同一個地址的鍵會構成一個單向鏈表。
在rehash對哈希表進行擴展或者收縮過程當中,會將全部鍵值對進行遷移,而且這個遷移是漸進式的遷移。
跳躍表(skiplist)是一種有序的數據結構,它經過每一個節點中維持多個指向其餘節點的指針,從而實現快速訪問。
跳躍表平均O(logN),最壞O(N),支持順序遍歷查找。
在redis中,有序集合(sortedset)的其中一種實現方式就是跳躍表。
當有序集合的元素較多,或者集合中的元素是比較常的字符串,則會使用跳躍表來實現。
跳躍表是由各個跳躍表節點組成。
1 /* ZSETs use a specialized version of Skiplists */ 2 /* 3 * 跳躍表節點 4 */ 5 typedef struct zskiplistNode { 6 7 // 成員對象 8 robj *obj; 9 10 // 分值 11 double score; 12 13 // 後退指針 14 struct zskiplistNode *backward; 15 16 // 層 17 struct zskiplistLevel { 18 19 // 前進指針 20 struct zskiplistNode *forward; 21 22 // 跨度 23 unsigned int span; 24 25 } level[]; 26 27 } zskiplistNode;
1 /* 2 * 跳躍表 3 */ 4 typedef struct zskiplist { 5 6 // 表頭節點和表尾節點 7 struct zskiplistNode *header, *tail; 8 9 // 表中節點的數量 10 unsigned long length; 11 12 // 表中層數最大的節點的層數 13 int level; 14 15 } zskiplist;
上圖最左邊就是跳躍表的結構:
header和tail:是跳躍表節點的頭結點和尾節點,
length:是跳躍表的長度(即跳躍表節點的數量,不含頭結點),
level:表示層數中最大節點的層數(不計算表頭結點)。
所以,獲取跳躍表的表頭、表尾、最大層數、長度的時間複雜度都是O(1)。
跳躍表節點:
層:節點中用L1,L2表示各層,每一個層都有兩個屬性,前進指針(forward)和跨度(span)。每一個節點的層高是1到32的隨機數
前進指針:用於訪問表尾方向的節點,便於跳躍表正向遍歷節點的時候,查找下一個節點位置;
跨度:記錄前進指針所指的節點和當前節點的距離,用於計算排位,訪問過程當中,將沿途訪問的全部層的跨度累計起來,獲得的結果就是跳躍表的排位。
後退指針:節點中用BW來表示,其指向當前節點的前一個節點,用於反向遍歷時候使用。每次只能後退至前一個節點。
分值:各節點中的數字就是分值,跳躍表中,節點按照分值從小到大排列。
成員對象:各個節點中,o1,o2是節點所保存的成員對象。是一個指針,指向一個字符串對象。
表頭節點也有後退指針,分值,成員對象,由於不會被用到,因此圖中省略。
分值能夠相同,成員對象必須惟一。
分值相同時,按照成員對象的字典序從小到大排。
跨度用來計算排位:
整數集合(intset)是集合鍵的底層實現之一,當一個集合只包含整數值元素,而且這個集合的元素數量很少時,Redis就會使用整數集合做爲集合鍵的底層實現。
它能夠保存類型爲int16_t、int32_t或者int64_t的整數值,而且保證集合中不會出現重複元素。
1 typedef struct intset { 2 // 編碼方式 3 uint32_t encoding; 4 // 集合包含的元素數量 5 uint32_t length; 6 // 保存元素的數組 7 int8_t contents[]; 8 } intset;
contents數組是整數集合的底層實現:整數集合的每一個元素都是contents數組的一個數組項,各個項在數組中按值的大小從小到大有序地排列,而且數組中不包含任何重複項。
升級:
每當咱們要將一個新元素添加到整數集合裏面,而且新元素的類型比整數集合現有全部元素的的類型都要長時,整數集合須要先進行升級,而後才能將新元素添加到整數集合裏面。
根據新元素的類型,擴展整數集合底層數組的空間大小,併爲新元素分配空間。
將底層數組現有的全部元素都轉換成與新元素相同的類型,並將類型轉換後的元素放置到正確的位上(從後往前),並且在放置元素的過程當中,須要繼續位置底層數組的有序性質不變。
將新元素添加到底層數組裏面。
將encoding屬性更改。
整數集合添加新元素的時間複雜度爲O(N)。
由於引起升級的元素要麼最大要麼最小,全部它的位置要麼是0要麼是length-1。
升級的好處:
提高整數集合的靈活性,能夠隨意將int16,int32,int64的值放入集合。
儘量地節約內存
降級:
整數集合不支持降級操做
壓縮列表(ziplist)是列表鍵和哈希鍵的底層實現之一。
當一個列表鍵只包含少許列表項,而且每一個列表項要麼就是小整數值,要麼就是長度比較短的字符串,那麼Redis就會使用壓縮列表來作列表鍵的底層實現。
壓縮列表是Redis爲了節約內存而開發的,是由一系列特殊編碼的連續內存塊組成的順序型數據結構。
一個壓縮列表有一下幾個組成部分:
每一個壓縮列表節點能夠保存一個字節數組或者一個整數值,而每一個節點都由previous_entry_length、encoding、content三個部分組成。
節點的previous_entry_length屬性以字節爲單位,記錄了壓縮列表中前一個節點的長度。
由於有了這個長度,因此程序能夠經過指針運算,根據當前節點的起始地址來計算出前一個節點的起始地址。
壓縮列表的從表尾向表頭遍歷操做就是使用這一原理實現的。
節點的encoding屬性記錄了節點的content屬性所保存數據的類型以及長度
節點的content屬性負責保存節點的值,節點值能夠是一個字節數組或者整數,值的類型和長度由節點的encoding屬性決定。
連鎖更新:
因爲previous_entry_length多是一個或者五個字節,全部插入和刪除操做帶來的連鎖更新在最壞狀況下須要對壓縮列表執行N次空間重分配操做,而每次空間重分配的最壞複雜度爲O(N),全部連鎖更新的最壞複雜度爲O(N^2)。
但連鎖更新的條件比較苛刻,並且壓縮列表中的數據量也不會太多,所以不須要注意性能問題,平均複雜度仍然是O(N)。
Redis對象系統中包含字符串對象、列表對象、哈希對象、集合對象、有序集合對象。
實現了基於引用計數的內存回收機制。
Redis使用對象來表示數據庫中的鍵和值。
/* * Redis 對象 */ typedef struct redisObject { // 類型 unsigned type:4; // 不使用(對齊位) unsigned notused:2; // 編碼方式 unsigned encoding:4; // LRU 時間(相對於 server.lruclock) unsigned lru:22; // 引用計數 int refcount; // 指向對象的值 void *ptr; } robj;
type表示了該對象的對象類型:
REDIS_STRING 字符串對象
REDIS_LIST 列表對象
REDIS_HASH 哈希對象
REDIS_SET 集合對象
REDIS_ZSET 有序集合對象
SET msg 「Hello World」
TYPE msg
輸出 string
OBJECT ENCODING msg
輸出 embstr
字符串對象的編碼能夠是int、raw、embstr
若是值是字符串對象,且長度大於32字節,那麼編碼爲raw
若是值是字符串對象,且長度小於等於32字節,那麼編碼爲embstr
embstr的建立只需分配一次內存,而raw爲兩次,分別建立redisObject結構和sdshdr結構。
相對地,embstr釋放內存的次數也由兩次變爲一次。
embstr的objet和sds放在一塊兒,更好地利用緩存帶來的優點。
redis並未提供任何修改embstr的方式,即embstr是隻讀的形式。對embstr的修改其實是先轉換爲raw再進行修改。
列表對象的編碼能夠是ziplist或者linkedlist。
當列表對象同時知足下面兩個條件時,則使用ziplist:
全部字符串元素的長度都小於64字節
元素數量小於512
ziplist是一種壓縮列表,它的好處是更能節省內存空間,由於它所存儲的內容都是在連續的內存區域當中的。當列表對象元素不大,每一個元素也不大的時候,就採用ziplist存儲。但當數據量過大時就ziplist就不是那麼好用了。由於爲了保證他存儲內容在內存中的連續性,插入的複雜度是O(N),即每次插入都會從新進行realloc。以下圖所示,對象結構中ptr所指向的就是一個ziplist。整個ziplist只須要malloc一次,它們在內存中是一塊連續的區域。
linkedlist是一種雙向鏈表。它的結構比較簡單,節點中存放pre和next兩個指針,還有節點相關的信息。當每增長一個node的時候,就須要從新malloc一塊內存。
哈希對象的底層實現能夠是ziplist或者hashtable。
當列表對象同時知足下面兩個條件時,則使用ziplist:
全部鍵值對的鍵和值的字符串度都小於64字節
鍵值對數量小於512
集合對象的編碼能夠是intset或者hashtable。
知足下面兩個條件,使用intset:
因此有元素都是整數值
元素數量不超過512個
有序集合的編碼可能兩種,一種是ziplist,另外一種是skiplist與dict的結合。
dict字典爲有序集合建立了一個成員到分值的映射。給一用O(1)的時間查到分值。
當有序集合對象同時知足下面兩個條件時,則使用ziplist:
全部元素的字符串度都小於64字節
元素數量小於128
默認下,Redis客戶端的目標數據庫爲0號數據庫。
SELECT 2 能夠切換到2號數據庫
經過EXPIRE或者PEXPIRE,能夠以秒或者毫秒爲鍵設置生存時間。服務器會自動刪除生存時間爲0的鍵。
數據庫主要由dict和expires兩個字典構成,其中dict負責保存鍵值對,expires負責保存鍵的過時時間。
服務器中的非空數據庫以及他們的鍵值對統稱爲數據庫狀態。
RDB持久化能夠將Redis在內存中的數據庫狀態保存到磁盤裏面,避免數據意外丟失。
RDB能夠手動執行,也能夠按期執行。能夠將某個時間點上的數據庫狀態保存到RDB文件中。經過該文件也能夠還原數據庫狀態。
RDB文件是一個通過壓縮的二進制文件,由多個部分組成。
對於不一樣類型的鍵值對,RDB文件會使用不一樣的方式來保存它們。
有兩個命令能夠生成Redis文件,SAVE和BGSAVE。
SAVE會阻塞服務器進程,直到RDB建立完成,期間不能處理任何命令請求。
BGSAVE會派生出一個子進程,由子進程建立RDB,父進程能夠繼續處理其餘命令請求。
BGSAVE執行時,客戶端發送的SAVE、BGSAVE這兩個命令會被服務器拒絕,BGREWRITEAOF會被延遲到BGSAVE執行完畢後執行。
服務器啓動時檢測到RDB文件存在,就會自動載入RDB文件。
若是服務器開啓了AOF持久化功能,服務器會優先使用AOF文件來還原數據庫狀態。
用戶能夠經過save選項設置多個保存條件,只要一個知足,服務器就會執行BGSAVE
save 900 1 ,900秒內對數據庫至少進行了1次修改
svae 300 10,300秒內對數據庫至少進行了10次修改
AOF(append only file)持久化是經過保存Redis服務器所執行的寫命令來記錄數據庫狀態。
實現可分爲命令追加(append)、文件寫入、文件同步(sync)三個步驟。
命令追加:
服務器在執行完一個寫命令後,會以協議格式將被執行的寫命令追加到服務器狀態的aof_buf緩衝區的末尾。
AOF文件的寫入與同步:
服務器在處理文件事件時可能會執行寫命令,使得一些內容被追加到緩衝區裏,因此在每次結束一個事件循環以前,會考慮是否將緩衝區的內容寫入到AOF文件裏。
Redis能夠建立一個新的AOF文件來替代現有的AOF文件,雖然數據庫狀態相同,但新的AOF文件不會包含任何浪費空間的冗餘命令,新文件體積會小不少。
實現:不是讀取現有AOF文件,而是根據現有數據庫狀態,用最少的命令去獲得這個狀態。
一個Redis集羣一般由多個節點組成,剛開始時,每一個節點是相互獨立的。咱們必須將各個獨立的節點鏈接起來。
節點經過握手來將其餘節點添加到本身所處的集羣中。
127.0.0.1:7000>CLUSTER MEET 127.0.0.1 7001 能夠將7001添加到節點7000所在的集羣裏。
Redis集羣經過分片的方式來保存數據庫中的鍵值對,集羣的整個數據庫被分爲16384個槽(slot)。
數據庫中每一個鍵都屬於其中一個槽,每一個節點能夠處理0-16384個槽。
當16384個槽都有節點在處理時,集羣處於上線狀態,只要有一個槽沒有獲得處理,那麼集羣處於下線狀態。
127.0.0.1:7000>CLUSTER ADDSLOTS 0 1 2 3 4 ... 5000 能夠將槽0-5000指派給節點7000負責。
每一個節點都會記錄哪些槽指派給了本身,哪些槽指派給了其餘節點。
客戶端向節點發送鍵命令,節點要計算這個鍵屬於哪一個槽。
若是是本身負責這個槽,那麼直接執行命令,若是不是,向客戶端返回一個MOVED錯誤,指引客戶端轉向正確的節點。
從新分片操做能夠將任意數量已經指派給某個節點的槽改成指派給另外一個節點。
從新分片期間可能會出現這種狀況:屬於遷移槽的一部分鍵值對保存在源節點裏,而另外一部分保存在目標節點裏。
若是節點A正在遷移槽i到節點B,那麼當節點A沒能在本身數據庫中找到命令指定的數據庫鍵時,節點會向客戶端返回一個ASK錯誤,指引客戶端到節點B繼續查找。
Redis集羣的節點分爲主節點和從節點。
主節點用於處理槽,從節點用於複製某個主節點,並在被複制的主節點下線後,替代主節點。
集羣中的每一個節點會按期向其餘節點發送PING消息,以此來檢測對方是否在線。
Redis的發佈訂閱由PUBLISH、SUBSCRIBE、PSUBSCRIBE等命令組成。
SUBSCRIBE:客戶端能夠訂閱一個或多個頻道,成爲這些頻道的訂閱者。每當有客戶端向這些頻道發消息的時候,頻道的全部訂閱者均可以收到這條消息。
PSUBSCRIBE:客戶端能夠訂閱一個或多個模式,成爲這些模式的訂閱者。每當有客戶端向這些頻道發消息的時候,訂閱頻道以及與這個頻道相匹配的模式的訂閱者都會收到消息。
Redis將全部頻道的訂閱關係都保存在服務器狀態的pubsub_channels字典裏,鍵是被訂閱的頻道,值是一個鏈表,記錄了全部訂閱這個頻道的客戶端。
UNSUBSCRIBE用於退訂頻道。
Redis將全部模式的訂閱關係都保存在服務器狀態的pubsub_patterns鏈表裏。
鏈表節點中記錄了被訂閱的模式以及訂閱這個模式的客戶端。
PUNSUBSCRIBE用於退訂模式。
PUBLISH 頻道 消息 ,能夠將消息發送給頻道。
頻道以及與頻道相匹配的模式的訂閱者都會收到消息。
PUBSUB NUMSUB 【channel-1 ... channel-n】 接受任意多個頻道做爲輸入參數,返回這些頻道的訂閱者數量。
PUBSUB NUMPAT ,返回服務器當前被訂閱模式的數量。
Redis經過MULTI、EXEC、WATCH等命令來實現事務功能。
事務提供了一種將多個命令請求打包,而後一次性、按順序執行多個命令的機制。
事務在執行期間,服務器不會中斷事務去執行其餘命令。
事務首先以MULTI開始,接着多個命令放入事務中,最後由EXEC命令將這個事務提交。
MULTI命令能夠將執行該命令的客戶端從非事務狀態切換至事務狀態。
切換到事務狀態後,若是客戶端發送的命令爲EXEC、DISCARD、WATCH、MULTI,那麼服務器會當即執行,其餘命令則會放入事務隊列裏。
處於事務狀態時,EXEC會被當即執行。服務器會遍歷事務隊列,執行隊列中的全部命令,最後將結果返回給客戶端。
WATCH命令是一個樂觀鎖,它能夠在EXEC命令執行以前,監視任意數量的數據庫鍵,並在EXEC執行時,檢查被監視的鍵是否至少有一個被修改過了,若是是,那麼服務器將拒絕執行事務。
每一個Redis數據庫都保存着一個watched_keys字典,這個字典的鍵是被WATCH監視的鍵,值是一個鏈表,記錄了全部監視相應數據庫鍵的客戶端。
若是某個鍵被修改,那麼監視該鍵的客戶端的REDIS_DIRTY_CAS標識就會被打開。
執行EXEC時,服務器會根據客戶端的REDIS_DIRTY_CAS標識是否被打開來決定是否執行事務。
Redis的事務和傳統的關係型事務最大的區別在於,Redis不支持事務回滾機制,即便事務隊列中的某個命令在執行期間出現錯誤,整個事務也會繼續執行下去。
若是一個事務在入隊命令過程當中,出現了命令不存在或者命令格式錯誤,那麼Redis將拒絕執行這個事務。
事務執行過程當中,出錯的命令不會對數據庫作任何修改。
只有當服務器運行在AOF持久化模式下,而且appendfsync爲always時,這種配置的事務才具備持久性。