第2章 簡單動態字符串(SDS)
redis的字符串不是直接用c語言的字符串,而是用了一種稱為簡單動態字符串(SDS)的抽象類型,並將其作為默認字符串。
redis中包含字符串值的鍵值對在底層都是由SDS實現的。
2.1 SDS定義
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字符串庫函數里的函數。
2.2 SDS與C語言字符串的區別
1、常數復雜度獲取字符串長度
C語言如果要獲取字符串的長度,需要從第一個字符開始,遍歷整個字符串,直到遍歷到\0符號,時間復雜度是O(N),即字符串的長度。
而redis由於已經存儲了字符串的長度,因此,時間復雜度是O(1)。
這樣,避免了獲取大字符串長度時時間的緩慢。
2、杜絕緩沖區溢出
C語言給字符串開辟一個存儲空間,如果對此存儲空間的使用超過開辟的空間,會導致內存溢出。
例如使用字符串拼接等方式時,就很容易出現此問題。而如果每次拼接之前都要計算每個字符串的長度,時間上又要耗費很久。
redis的SDS中內置一個sdscat函數,也是用於字符串的拼接。但是在執行操作之前,其會先檢查空間是否足夠。
如果free的值不夠,會再申請內存空間,避免溢出。
3、減少內存分配次數
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中未使用的空間,因此也不需要擔心內存泄漏問題。
4、二進制安全
SDS 的 API 都是二進制安全的: 所有 SDS API 都會以處理二進制的方式來處理 SDS 存放在 buf 數組里的數據, 程序不會對其中的數據做任何限制、過濾、或者假設 —— 數據在寫入時是什么樣的, 它被讀取時就是什么樣。
sds考慮字符串長度,是通過len屬性,而不是通過\0來判斷。
5、兼容部分C語言字符串函數
redis兼容c語言對於字符串末尾采用\0進行處理,這樣使得其可以復用部分c語言字符串函數的代碼,實現代碼的精簡性。
第3章 鏈表
列表鍵的底層之一是鏈表。(底層也有可能是壓縮列表)
當列表鍵包含了許多元素,或者元素是比較長的字符串的時候,就會用到鏈表作為列表鍵的底層實現。
3.1鏈表和表節點的實現
1、節點結構
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組成雙向鏈表,如下圖所示:

2、鏈表結構
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三個屬性為節點值設置類型特定函數,所以鏈表可以用於保存不同類型的值。
第4章 字典
字典,又稱符號表、關聯數組、映射,是一種保存鍵值對的抽象數據結構。
每個鍵(key)和唯一的值(value)關聯,鍵是獨一無二的,通過對鍵的操作可以對值進行增刪改查。
redis中字典應用廣泛,對redis數據庫的增刪改查就是通過字典實現的。即redis數據庫的存儲,和大部分關系型數據庫不同,不采用B+tree進行處理,而是采用hash的方式進行處理。
字典還是hash鍵的底層實現之一。
當hash鍵包含了許多元素,或者元素是比較長的字符串的時候,就會用到字典作為hash鍵的底層實現。
4.1 字典的實現
redis的字典,底層是使用哈希表實現,每個哈希表有多個哈希節點,每個哈希節點保存了一個鍵值對。
1、哈希表
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的空哈希表結構如下圖(左邊一列的圖)所示:

2、哈希表節點
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是指向另一個哈希表節點的指針,該指針將多個哈希值相同的鍵值對連接在一起,避免因為哈希值相同導致的沖突。
哈希表節點如下圖(左邊第一列是哈希表結構,表節點結構從左邊第二列開始)所示:

3、字典
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]。
完整的字典結構如下圖所示:

4.2 哈希算法
要將新的鍵值對加到字典,程序要先對鍵進行哈希算法,算出哈希值和索引值,再根據索引值,把包含新鍵值對的哈希表節點放到哈希表數組指定的索引上。
redis實現哈希的代碼是:
hash =dict->type->hashFunction(key); index = hash& dict->ht[x].sizemask;
算出來的結果中,index的值是多少,則key會落在table里面的第index個位置(第一個位置index是0)。
其中,redis的hashFunction,采用的是murmurhash2算法,是一種非加密型hash算法,其具有高速的特點。
4.3 鍵沖突解決
當兩個或者以上的鍵被分配到哈希表數組的同一個索引上,則稱這些鍵發生了沖突。
為了解決此問題,redis采用鏈地址法。被分配到同一個索引上的多個節點可以用單鏈表連接起來。
因為沒有指向尾節點的指針,所以總是將新節點加在表頭的位置。(O(1)時間)
4.4 rehash(重新散列)
隨着操作進行,哈希表保存的鍵值對會增加或減少,為了讓哈希表的負載因子(load factor)維持在一個合理范圍,當一個哈希表保存的鍵太多或者太少,需要對哈希表進行擴展或者收縮。擴展或收縮哈希表的過程,就稱為rehash。
rehash步驟如下:
1、給字典的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。
2、將保存在ht[0]上面的所有鍵值對,rehash到ht[1],即對每個鍵重新采用哈希算法的方式計算哈希值和索引值,再放到相應的ht[1]的表格指定位置。
3、當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自動開始哈希表的收縮工作。
4.5 漸進式rehash
redis對ht[0]擴展或收縮到ht[1]的過程,並不是一次性完成的,而是漸進式、分多次的完成,以避免如果哈希表中存有大量鍵值對,一次性復制過程中,占用資源較多,會導致redis服務停用的問題。
漸進式rehash過程如下:
1、為ht[1]分配空間,讓字典同時持有ht[0]和ht[1]兩張哈希表。
2、將字典中的rehashidx設置成0,表示正在rehash。rehashidx的值默認是-1,表示沒有在rehash。
3、在rehash進行期間,程序處理正常對字典進行增刪改查以外,還會順帶將ht[0]哈希表上,rehashidx索引上,所有的鍵值對數據rehash到ht[1],並且rehashidx的值加1。
4、當某個時間節點,全部的ht[0]都遷移到ht[1]后,rehashidx的值重新設定為-1,表示rehash完成。
漸進式rehash采用分而治之的工作方式,將哈希表的遷移工作所耗費的時間,平攤到增刪改查中,避免集中rehash導致的龐大計算量。
在rehash期間,對哈希表的查找、修改、刪除,會先在ht[0]進行。
如果ht[0]中沒找到相應的內容,則會去ht[1]查找,並進行相關的修改、刪除操作。而增加的操作,會直接增加到ht[1]中,目的是讓ht[0]只減不增,加快遷移的速度。
4.6 總結
字典在redis中廣泛應用,包括數據庫和hash數據結構。
每個字典有兩個哈希表,一個是正常使用,一個用於rehash期間使用。
當redis計算哈希時,采用的是MurmurHash2哈希算法。
哈希表采用鏈地址法避免鍵的沖突,被分配到同一個地址的鍵會構成一個單向鏈表。
在rehash對哈希表進行擴展或者收縮過程中,會將所有鍵值對進行遷移,並且這個遷移是漸進式的遷移。
第5章 跳躍表
跳躍表(skiplist)是一種有序的數據結構,它通過每個節點中維持多個指向其他節點的指針,從而實現快速訪問。
跳躍表平均O(logN),最壞O(N),支持順序遍歷查找。
在redis中,有序集合(sortedset)的其中一種實現方式就是跳躍表。
當有序集合的元素較多,或者集合中的元素是比較常的字符串,則會使用跳躍表來實現。
5.1 跳躍表實現
跳躍表是由各個跳躍表節點組成。
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是節點所保存的成員對象。是一個指針,指向一個字符串對象。
表頭節點也有后退指針,分值,成員對象,因為不會被用到,所以圖中省略。
分值可以相同,成員對象必須唯一。
分值相同時,按照成員對象的字典序從小到大排。
跨度用來計算排位:
第6章 整數集合
整數集合(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的值放入集合。
盡可能地節約內存
降級:
整數集合不支持降級操作
第7章 壓縮列表
壓縮列表(ziplist)是列表鍵和哈希鍵的底層實現之一。
當一個列表鍵只包含少量列表項,並且每個列表項要么就是小整數值,要么就是長度比較短的字符串,那么Redis就會使用壓縮列表來做列表鍵的底層實現。
壓縮列表是Redis為了節約內存而開發的,是由一系列特殊編碼的連續內存塊組成的順序型數據結構。
一個壓縮列表有一下幾個組成部分:
每個壓縮列表節點可以保存一個字節數組或者一個整數值,而每個節點都由previous_entry_length、encoding、content三個部分組成。
previous_entry_length:
節點的previous_entry_length屬性以字節為單位,記錄了壓縮列表中前一個節點的長度。
因為有了這個長度,所以程序可以通過指針運算,根據當前節點的起始地址來計算出前一個節點的起始地址。
壓縮列表的從表尾向表頭遍歷操作就是使用這一原理實現的。
encoding:
節點的encoding屬性記錄了節點的content屬性所保存數據的類型以及長度
content:
節點的content屬性負責保存節點的值,節點值可以是一個字節數組或者整數,值的類型和長度由節點的encoding屬性決定。
連鎖更新:
由於previous_entry_length可能是一個或者五個字節,所有插入和刪除操作帶來的連鎖更新在最壞情況下需要對壓縮列表執行N次空間重分配操作,而每次空間重分配的最壞復雜度為O(N),所有連鎖更新的最壞復雜度為O(N^2)。
但連鎖更新的條件比較苛刻,而且壓縮列表中的數據量也不會太多,因此不需要注意性能問題,平均復雜度仍然是O(N)。
第8章 對象
Redis對象系統中包含字符串對象、列表對象、哈希對象、集合對象、有序集合對象。
實現了基於引用計數的內存回收機制。
8.1 對象的類型與編碼
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
8.2 字符串對象
字符串對象的編碼可以是int、raw、embstr
如果值是字符串對象,且長度大於32字節,那么編碼為raw
如果值是字符串對象,且長度小於等於32字節,那么編碼為embstr
embstr的創建只需分配一次內存,而raw為兩次,分別創建redisObject結構和sdshdr結構。
相對地,embstr釋放內存的次數也由兩次變為一次。
embstr的objet和sds放在一起,更好地利用緩存帶來的優勢。
redis並未提供任何修改embstr的方式,即embstr是只讀的形式。對embstr的修改實際上是先轉換為raw再進行修改。
8.3 列表對象
列表對象的編碼可以是ziplist或者linkedlist。
當列表對象同時滿足下面兩個條件時,則使用ziplist:
所有字符串元素的長度都小於64字節
元素數量小於512
ziplist是一種壓縮列表,它的好處是更能節省內存空間,因為它所存儲的內容都是在連續的內存區域當中的。當列表對象元素不大,每個元素也不大的時候,就采用ziplist存儲。但當數據量過大時就ziplist就不是那么好用了。因為為了保證他存儲內容在內存中的連續性,插入的復雜度是O(N),即每次插入都會重新進行realloc。如下圖所示,對象結構中ptr所指向的就是一個ziplist。整個ziplist只需要malloc一次,它們在內存中是一塊連續的區域。
linkedlist是一種雙向鏈表。它的結構比較簡單,節點中存放pre和next兩個指針,還有節點相關的信息。當每增加一個node的時候,就需要重新malloc一塊內存。
8.4 哈希對象
哈希對象的底層實現可以是ziplist或者hashtable。
當列表對象同時滿足下面兩個條件時,則使用ziplist:
所有鍵值對的鍵和值的字符串度都小於64字節
鍵值對數量小於512
8.5 集合對象
集合對象的編碼可以是intset或者hashtable。
滿足下面兩個條件,使用intset:
所以有元素都是整數值
元素數量不超過512個
8.6 有序集合對象
有序集合的編碼可能兩種,一種是ziplist,另一種是skiplist與dict的結合。
dict字典為有序集合創建了一個成員到分值的映射。給一用O(1)的時間查到分值。
當有序集合對象同時滿足下面兩個條件時,則使用ziplist:
所有元素的字符串度都小於64字節
元素數量小於128
第9章 數據庫
默認下,Redis客戶端的目標數據庫為0號數據庫。
SELECT 2 可以切換到2號數據庫
通過EXPIRE或者PEXPIRE,可以以秒或者毫秒為鍵設置生存時間。服務器會自動刪除生存時間為0的鍵。
數據庫主要由dict和expires兩個字典構成,其中dict負責保存鍵值對,expires負責保存鍵的過期時間。
第10章 RDB持久化
服務器中的非空數據庫以及他們的鍵值對統稱為數據庫狀態。
RDB持久化可以將Redis在內存中的數據庫狀態保存到磁盤里面,避免數據意外丟失。
RDB可以手動執行,也可以定期執行。可以將某個時間點上的數據庫狀態保存到RDB文件中。通過該文件也可以還原數據庫狀態。
RDB文件是一個經過壓縮的二進制文件,由多個部分組成。
對於不同類型的鍵值對,RDB文件會使用不同的方式來保存它們。
10.1 RDB文件的創建和載入
有兩個命令可以生成Redis文件,SAVE和BGSAVE。
SAVE會阻塞服務器進程,直到RDB創建完成,期間不能處理任何命令請求。
BGSAVE會派生出一個子進程,由子進程創建RDB,父進程可以繼續處理其他命令請求。
BGSAVE執行時,客戶端發送的SAVE、BGSAVE這兩個命令會被服務器拒絕,BGREWRITEAOF會被延遲到BGSAVE執行完畢后執行。
服務器啟動時檢測到RDB文件存在,就會自動載入RDB文件。
如果服務器開啟了AOF持久化功能,服務器會優先使用AOF文件來還原數據庫狀態。
10.2 自動間隔保存
用戶可以通過save選項設置多個保存條件,只要一個滿足,服務器就會執行BGSAVE
save 900 1 ,900秒內對數據庫至少進行了1次修改
svae 300 10,300秒內對數據庫至少進行了10次修改
第11章 AOF持久化
AOF(append only file)持久化是通過保存Redis服務器所執行的寫命令來記錄數據庫狀態。
11.1 AOF持久化的實現
實現可分為命令追加(append)、文件寫入、文件同步(sync)三個步驟。
命令追加:
服務器在執行完一個寫命令后,會以協議格式將被執行的寫命令追加到服務器狀態的aof_buf緩沖區的末尾。
AOF文件的寫入與同步:
服務器在處理文件事件時可能會執行寫命令,使得一些內容被追加到緩沖區里,所以在每次結束一個事件循環之前,會考慮是否將緩沖區的內容寫入到AOF文件里。
11.3 AOF重寫
Redis可以創建一個新的AOF文件來替代現有的AOF文件,雖然數據庫狀態相同,但新的AOF文件不會包含任何浪費空間的冗余命令,新文件體積會小很多。
實現:不是讀取現有AOF文件,而是根據現有數據庫狀態,用最少的命令去得到這個狀態。
第17章 集群
17.1 節點
一個Redis集群通常由多個節點組成,剛開始時,每個節點是相互獨立的。我們必須將各個獨立的節點連接起來。
節點通過握手來將其他節點添加到自己所處的集群中。
127.0.0.1:7000>CLUSTER MEET 127.0.0.1 7001 可以將7001添加到節點7000所在的集群里。
17.2 槽指派
Redis集群通過分片的方式來保存數據庫中的鍵值對,集群的整個數據庫被分為16384個槽(slot)。
數據庫中每個鍵都屬於其中一個槽,每個節點可以處理0-16384個槽。
當16384個槽都有節點在處理時,集群處於上線狀態,只要有一個槽沒有得到處理,那么集群處於下線狀態。
127.0.0.1:7000>CLUSTER ADDSLOTS 0 1 2 3 4 ... 5000 可以將槽0-5000指派給節點7000負責。
每個節點都會記錄哪些槽指派給了自己,哪些槽指派給了其他節點。
17.3 在集群中執行命令
客戶端向節點發送鍵命令,節點要計算這個鍵屬於哪個槽。
如果是自己負責這個槽,那么直接執行命令,如果不是,向客戶端返回一個MOVED錯誤,指引客戶端轉向正確的節點。
17.4 重新分片
重新分片操作可以將任意數量已經指派給某個節點的槽改為指派給另一個節點。
17.5 ASK錯誤
重新分片期間可能會出現這種情況:屬於遷移槽的一部分鍵值對保存在源節點里,而另一部分保存在目標節點里。
如果節點A正在遷移槽i到節點B,那么當節點A沒能在自己數據庫中找到命令指定的數據庫鍵時,節點會向客戶端返回一個ASK錯誤,指引客戶端到節點B繼續查找。
17.6 復制與故障轉移
Redis集群的節點分為主節點和從節點。
主節點用於處理槽,從節點用於復制某個主節點,並在被復制的主節點下線后,替代主節點。
集群中的每個節點會定期向其他節點發送PING消息,以此來檢測對方是否在線。
第18章 發布和訂閱
Redis的發布訂閱由PUBLISH、SUBSCRIBE、PSUBSCRIBE等命令組成。
SUBSCRIBE:客戶端可以訂閱一個或多個頻道,成為這些頻道的訂閱者。每當有客戶端向這些頻道發消息的時候,頻道的所有訂閱者都可以收到這條消息。
PSUBSCRIBE:客戶端可以訂閱一個或多個模式,成為這些模式的訂閱者。每當有客戶端向這些頻道發消息的時候,訂閱頻道以及與這個頻道相匹配的模式的訂閱者都會收到消息。
18.1 頻道的訂閱與退訂
Redis將所有頻道的訂閱關系都保存在服務器狀態的pubsub_channels字典里,鍵是被訂閱的頻道,值是一個鏈表,記錄了所有訂閱這個頻道的客戶端。
UNSUBSCRIBE用於退訂頻道。
18.2 模式的訂閱與退訂
Redis將所有模式的訂閱關系都保存在服務器狀態的pubsub_patterns鏈表里。
鏈表節點中記錄了被訂閱的模式以及訂閱這個模式的客戶端。
PUNSUBSCRIBE用於退訂模式。
18.3 發送消息
PUBLISH 頻道 消息 ,可以將消息發送給頻道。
頻道以及與頻道相匹配的模式的訂閱者都會收到消息。
18.4 查看訂閱消息
PUBSUB NUMSUB 【channel-1 ... channel-n】 接受任意多個頻道作為輸入參數,返回這些頻道的訂閱者數量。
PUBSUB NUMPAT ,返回服務器當前被訂閱模式的數量。
第19章 事務
Redis通過MULTI、EXEC、WATCH等命令來實現事務功能。
事務提供了一種將多個命令請求打包,然后一次性、按順序執行多個命令的機制。
事務在執行期間,服務器不會中斷事務去執行其他命令。
事務首先以MULTI開始,接着多個命令放入事務中,最后由EXEC命令將這個事務提交。
19.1 事務的實現
MULTI命令可以將執行該命令的客戶端從非事務狀態切換至事務狀態。
切換到事務狀態后,如果客戶端發送的命令為EXEC、DISCARD、WATCH、MULTI,那么服務器會立即執行,其他命令則會放入事務隊列里。
處於事務狀態時,EXEC會被立即執行。服務器會遍歷事務隊列,執行隊列中的所有命令,最后將結果返回給客戶端。
19.2 WATCH命令的實現
WATCH命令是一個樂觀鎖,它可以在EXEC命令執行之前,監視任意數量的數據庫鍵,並在EXEC執行時,檢查被監視的鍵是否至少有一個被修改過了,如果是,那么服務器將拒絕執行事務。
每個Redis數據庫都保存着一個watched_keys字典,這個字典的鍵是被WATCH監視的鍵,值是一個鏈表,記錄了所有監視相應數據庫鍵的客戶端。
如果某個鍵被修改,那么監視該鍵的客戶端的REDIS_DIRTY_CAS標識就會被打開。
執行EXEC時,服務器會根據客戶端的REDIS_DIRTY_CAS標識是否被打開來決定是否執行事務。
19.3 事務的ACID特性
Redis的事務和傳統的關系型事務最大的區別在於,Redis不支持事務回滾機制,即使事務隊列中的某個命令在執行期間出現錯誤,整個事務也會繼續執行下去。
如果一個事務在入隊命令過程中,出現了命令不存在或者命令格式錯誤,那么Redis將拒絕執行這個事務。
事務執行過程中,出錯的命令不會對數據庫做任何修改。
只有當服務器運行在AOF持久化模式下,並且appendfsync為always時,這種配置的事務才具有持久性。