《Redis設(shè)計與實現(xiàn)》讀書筆記

第二章 簡單動態(tài)字符串 (SDS)

1凡蚜、SDS的定義

struct sdshdr {
      // 記錄數(shù)組中已經(jīng)使用的字節(jié)數(shù)控,等于SDS所保存的字符串的長度
      int len;

       // 記錄buf數(shù)組中未使用的字節(jié)數(shù)量 
       int free;
       
       // 字節(jié)數(shù)組耻蛇,用于保存字符串
       char[] buf;
}

比如踪蹬,free為0,則表示SDS沒有分配任何未使用的空間臣咖,len屬性位5跃捣,則表示這個SDS保存了一個五字節(jié)長的字符串,buf的最后一個字符為'\0'夺蛇,這和c的字符串是一樣的疚漆;
需要說明的是,在C語言中使用長度為N+1的字符數(shù)組來表示長度為N的字符串刁赦,最后一個空字符為'\0'娶聘,表示字符串結(jié)束。

2甚脉、SDS和C字符串的區(qū)別

2.1 常數(shù)復(fù)雜度獲取字符串長度

對于C字符串來說丸升,因為沒有記錄字符串長度,所以如果想要知道字符串長度牺氨,則需要遍歷一次字符串狡耻,這個操作的時間復(fù)雜度為O(N)墩剖,而SDS中的len屬性記錄了SDS當(dāng)前字符串的長度,所以可以直接獲取夷狰,這個操作的時間復(fù)雜度是O(1)岭皂;而且SDS的len屬性是在操作SDS API的時候自動完成的,不需要自己維護(hù)沼头,所以不需要擔(dān)心安全問題爷绘;

2.2 SDS杜絕緩沖區(qū)溢出

因為C字符串不記錄自身長度的原因,所以容易造成緩沖區(qū)溢出瘫证,比如揉阎,有兩個字符串在內(nèi)存中緊挨著:

...   R E D I S \0 M O N G O D B \0 ...

其中S1為字符串 “REDIS”,s2為字符串 “MONGODB”背捌,C語言中有個字符串操作函數(shù) strcat(char* dest, har* src) 可以將src字符串拼接到dest字符串的后面毙籽,如果dest剩余的內(nèi)存空間無法再容納src,則就會發(fā)生緩沖區(qū)溢出毡庆,比如執(zhí)行:
strcat(S1, "CLUSTER")坑赡,內(nèi)存中的S2字符串就會被覆蓋掉:

...   R E D I S  C L U S T E R \0 ...

而SDS在進(jìn)行字符串修改之前,會先檢查SDS的空間是否滿足修改所需的空間要求么抗,如果不滿足毅否,則會先擴(kuò)展SDS的空間,然后再進(jìn)行修改蝇刀。

2.3 SDS減少修改字符串時帶來的內(nèi)存重分配次數(shù)

對于C字符串來說螟加,將字符串變長或者縮短之前,都需要進(jìn)行內(nèi)存重新分配吞琐,否則會出現(xiàn)問題捆探,如果字符串變長之前不進(jìn)行內(nèi)存重新分配,則會出現(xiàn)緩沖區(qū)溢出問題站粟,如果在縮短字符串之前不進(jìn)行內(nèi)存重分配黍图,則會發(fā)生內(nèi)存泄漏問題;

SDS使用free屬性來代表buf中還有多少字符可以使用奴烙,SDS使用空間預(yù)分配和惰性空間釋放策略來避免這個問題助被;

2.3.1 空間預(yù)分配

SDS在需要對空間進(jìn)行擴(kuò)展的時候,會額外擴(kuò)展一些空間切诀,使用free屬性來維護(hù)揩环,避免頻繁分配內(nèi)存;額外分配的內(nèi)存大小由以下公式?jīng)Q定:

  • 如果對SDS進(jìn)行修改之后幅虑,SDS的長度(len屬性)將小于1MB丰滑,那么程序?qū)⒎峙浜蚻en屬性同樣大小的未使用空間,比如翘单,在SDS進(jìn)行修改之后吨枉,len屬性變?yōu)?3字節(jié),那么程序也會分配13字節(jié)未使用的空間(free屬性)哄芜,此時free和len相等貌亭,buf的實際長度為13 + 1 + 13 = 27字節(jié);
  • 如果對SDS進(jìn)行修改之后认臊,SDS的長度大于1MB空間圃庭,那么SDS會分配1MB的未使用空間;

2.3.2 惰性空間釋放

在SDS字符串進(jìn)行縮短字符串操作時失晴,SDS并不會執(zhí)行內(nèi)存重分配將對于的空間進(jìn)行回收剧腻,而是記錄在free屬性里面,等待后續(xù)字符串?dāng)U大時使用涂屁,這就避免了字符串造成時的頻繁內(nèi)存操作书在;當(dāng)然,如果確認(rèn)字符串空間已經(jīng)足夠拆又,那么可以調(diào)用SDS的API進(jìn)行內(nèi)存回收儒旬。

2.4 二進(jìn)制安全

C語言中使用空字符'\0'來表示字符串的末尾,也就是字符串中間不能存在空字符串帖族,否則字符串就會被截斷栈源,而對于一些二進(jìn)制數(shù)據(jù),難免會使用到空字符竖般,而C字符串就無法表示這些數(shù)據(jù)甚垦,SDS使用buf來存儲字符串,存儲的是什么涣雕,讀出來的就是什么艰亮,是二進(jìn)制安全的表示方式。所以Redis不僅可以保存字符串胞谭,還可以保存二進(jìn)制文件垃杖,比如視頻、圖片等丈屹。

2.5 兼容部分C字符串函數(shù)

SDS的buf里面存儲的字符串依然保持以'\0'結(jié)尾的慣例调俘,所以可以使用部分C字符串函數(shù),比如strcat旺垒,這樣彩库,SDS就不需要再重復(fù)編寫一遍字符串操作函數(shù)了。

第三章 鏈表

3.1 Redis鏈表實現(xiàn)的特性

  • 雙端鏈表:每個鏈表節(jié)點帶有prev和next指針先蒋,獲取某個節(jié)點的前置或者后置節(jié)點的時間復(fù)雜度為O(1)骇钦;
  • 無環(huán):表頭的prev和表尾的next都指向NULL,對鏈表的訪問以NULL為結(jié)尾竞漾;
  • 帶表頭指針和表位指針:通過list的head和tail指針眯搭,獲取表頭或者表尾的時間復(fù)雜度為O(1)窥翩;
  • 帶鏈表長度計數(shù)器:使用list結(jié)構(gòu)的len屬性來表示鏈表的長度,程序獲取鏈表長度的時間復(fù)雜度為O(1)
  • 多態(tài):鏈表的每個節(jié)點使用void*指針來保存節(jié)點的值鳞仙,并且通過dup寇蚊、free、match三個屬性為節(jié)點設(shè)置類型特定函數(shù)棍好,所以鏈表合一用于保存各種不同類型的值仗岸;

第四章 字典

字典:又被稱為符號表、關(guān)聯(lián)數(shù)組借笙、映射(map)扒怖,是哈希鍵的底層實現(xiàn)
Redis的字典使用哈希表作為底層實現(xiàn);

4.1 哈希表

dict.h/dictht

typedef struct dictht {
    
      // 哈希表數(shù)組
      dictEntry ** table;

      // 哈希表大小
      unsigned long size;

       // 哈希表大小掩碼业稼,用于計算索引值盗痒,總是等于 size - 1
       unsigned long sizemask;

       // 該哈希表已有節(jié)點的數(shù)量
       unsigned long used; 

} dictht;

4.2 哈希表節(jié)點

dictEntry 保存鍵值對信息。

typedef struct dictEntry {
    
      // 鍵
      void* key;

      // 值
      union {
            void* val;
              ...
       } v;

     // 下一個哈希節(jié)點盼忌,形成鏈表积糯,用于解決哈希沖突的問題
     struct dictEntry* next;

} dictEntry;

4.3字典

dict.h/dict

typedef struct dict {
   
      // 類型特定函數(shù)
      dictType* type;

      // 私有數(shù)據(jù)
      void* privdata;

      // 哈希表
      dicthd ht[2];

      // rehash 索引,當(dāng)rehash不在進(jìn)行時谦纱,值為-1
      int rehashidx;  

} dict;

ht屬性是包含兩個哈希表的數(shù)組看成,一般情況下,只有ht[0]會被使用跨嘉,ht[1]只有在rehash的時候才會用到川慌,rehashidx用于標(biāo)記rehash的進(jìn)度,如果當(dāng)前不在進(jìn)行rehash操作祠乃,則rehashidx為-1梦重;

4.4 哈希算法

當(dāng)要將一個新的鍵值對添加到字典中時,需要先根據(jù)鍵值對的鍵來計算出哈希值和索引值亮瓷,然后根據(jù)索引值將包含新鍵值對的哈希節(jié)點放到對應(yīng)的位置上去琴拧;
計算哈希值的函數(shù)在字典的type屬性里面;
當(dāng)一個數(shù)組的長度是2的冪次方時嘱支,有一個很好的特性是蚓胸,
index = hash & (len -1)
這可能就是sizemask的作用;

4.5 解決鍵沖突

當(dāng)有兩個以上的鍵被分配到同一個索引上的時候除师,稱這些鍵發(fā)生了沖突沛膳,Redis使用鏈地址法來解決哈希沖突,每個鍵值對節(jié)點都是一個鏈表節(jié)點汛聚,包含next指針锹安;

4.6 rehash

隨著操作的不斷執(zhí)行,哈希表保存的鍵值對會逐漸的增多或者減少,為了讓哈希表的負(fù)載因子保持在一個合理的范圍叹哭,當(dāng)哈希表的鍵值對太多或者太少時忍宋,程序需要進(jìn)行rehash操作。

Redis進(jìn)行rehash操作的步驟如下:

  • 為字典ht[1]分配空間风罩,分配的大小取決于要執(zhí)行的操作讶踪,和ht[0]當(dāng)前包含的鍵值對數(shù)量(ht[0].used)
    • 如果執(zhí)行的擴(kuò)展操作,那么ht[1]的大小為第一個大于等于 ht[0].used * 2 的2次冪泊交;
    • 如果執(zhí)行的是收縮操作,那么ht[1]的大小為第一個大于等于ht[0].used的2次冪柱查;
  • 將保存在ht[0]中的所有鍵值對rehash到ht[1]上面
  • 當(dāng)ht[0]的所有鍵值對都rehash到ht[1]之后廓俭,釋放ht[0],然后將ht[1]當(dāng)做ht[0]唉工;

4.7 哈希表的收縮與擴(kuò)展

當(dāng)以下條件任意一個滿足時研乒,程序會自動開始對哈希表進(jìn)行擴(kuò)展操作:

  • 服務(wù)器目前沒有執(zhí)行BGSAVE或者BGREWRITEAOF命令,并且哈希表的負(fù)載因子大于等于1淋硝;
  • 服務(wù)器正在執(zhí)行BGSAVE或者BGREWRITEAOF命令雹熬,并且哈希表的負(fù)載因子大于等于5;

負(fù)載因子的計算公式為:
load_factor = ht[0].used / ht[0].size

Redis在執(zhí)行BGSAVE或者BGREWRITEAOF命令時谣膳,需要fork出紫子進(jìn)程竿报,大多數(shù)操作系統(tǒng)采用寫時復(fù)制的技術(shù)來優(yōu)化進(jìn)程的內(nèi)存使用效率,如果在這個時候進(jìn)行rehash继谚,子進(jìn)程也需要寫烈菌,如果避免在這個時候進(jìn)行擴(kuò)展,則可以最大限度的節(jié)約內(nèi)存花履。

當(dāng)哈希表的負(fù)載因子小于0.1時芽世,程序會自動開始對哈希表進(jìn)行收縮操作。

4.8 漸進(jìn)式rehash

rehash操作并不是一次性完成的诡壁,因為哈希表中的鍵值對數(shù)量可能非常大济瓢,如果一次性完成所有鍵值對的rehash操作,那么Redis可能會停止服務(wù)而進(jìn)行rehash妹卿,為了避免這個問題旺矾,Redis采用分多次,漸進(jìn)式的完成rehash操作纽帖;

下面是Redis進(jìn)行漸進(jìn)式rehash的詳細(xì)步驟:

  • 為ht[1]分配空間宠漩,讓字典同時持有ht[0]和ht[1]兩個哈希表
  • 在字典中維護(hù)一個索引計數(shù)器變量rehashidx,并將它的值設(shè)置為0懊直,表示rehash工作正式開始扒吁;
  • 在rehash進(jìn)行期間,每次對字典的訪問操作,程序除了執(zhí)行指定的操作之外雕崩,還會順帶將ht[0]哈希表在rehashidx索引上的鍵值對rehash到ht[1]魁索,當(dāng)rehash完成之后,rehashidx + 1盼铁,表示下一次rehash操作的鍵值對索引是rehashidx + 1
  • 隨著字典的不斷操作粗蔚,到某個時間點,ht[0]上的所有鍵值對都會rehash到ht[1]饶火,此時設(shè)置rehashidx = -1鹏控,表示rehash已經(jīng)完成;

在rehash期間肤寝,字典會同時持有ht[0]和ht[1]兩張哈希表当辐,對于查找來說,會現(xiàn)在ht[0]找鲤看,找不到再去ht[1]找缘揪, 對于刪除操作也是需要操作兩張哈希表,而對于插入操作义桂,所有新插入的鍵值對只會在ht[1]里面找筝,所以保證了ht[0]里面的鍵值對只減不增,這樣rehash總是會結(jié)束慷吊。

第五章 跳躍表

跳躍表是有序列表的底層實現(xiàn)數(shù)據(jù)結(jié)構(gòu)袖裕,Redis的跳躍表實現(xiàn)包括zskiplist和zskiplistNode兩個結(jié)構(gòu),每個跳躍表的層高都是1~32之間的隨機(jī)數(shù)溉瓶,在同一個跳躍表中陆赋,每一個節(jié)點的成員變量必須唯一(類似于Set),當(dāng)多個節(jié)點的分值相同時嚷闭,節(jié)點按照成員對象的大小進(jìn)行排序攒岛。

https://redisbook.readthedocs.io/en/latest/internal-datastruct/skiplist.html
https://juejin.im/post/57fa935b0e3dd90057c50fbc

下面是一個跳躍表圖片:

skiplist

查找的時候先從最高層開始找,逐漸二分下降層數(shù)胞锰,整體上就是一個二分查找算法的最佳實踐遗淳。

下面是一個再跳躍表里面查找23這個值的例子:

search in skiplist

第六章 整數(shù)集合

  • 整數(shù)集合是集合鍵的底層實現(xiàn)之一奔害;
  • 整數(shù)集合的底層實現(xiàn)為數(shù)組,這個數(shù)組以有序,無重復(fù)的方式存儲集合元素呢簸,當(dāng)有需要的時候會改變數(shù)組的類型(encode)
  • 升級操作為整數(shù)集合帶來了操作上的靈活性褒链,并且盡可能的節(jié)約了內(nèi)存阻问;
  • 整數(shù)集合只支持升級操作昌简,不支持降級操作

第七章 壓縮列表

壓縮列表是列表鍵和哈希鍵的底層實現(xiàn)之一,當(dāng)一個列表鍵只包含少量的列表項帽蝶,并且每個列表項要么就是小整數(shù)值赦肋,要么就是簡短的字符串,那么Redis就會使用壓縮列表來進(jìn)行實現(xiàn);
當(dāng)一個哈希鍵只包含少量鍵值對佃乘,并且每個鍵值對的鍵和值要么是小整數(shù)囱井,要么是簡短的字符串,那么Redis就會使用壓縮列表來實現(xiàn)哈希鍵趣避。
壓縮列表是Redis為了節(jié)約內(nèi)存而開發(fā)的一種順序存儲數(shù)據(jù)結(jié)構(gòu)庞呕。

第八章 對象

Redis基于前面介紹的那些數(shù)據(jù)結(jié)構(gòu),構(gòu)建了一個對象系統(tǒng)程帕,這個系統(tǒng)包括字符串對象住练、列表對象、哈希對象愁拭、集合對象和有序集合對象五種類型的對象澎羞。
Redis通過引用計數(shù)技術(shù),當(dāng)程序不再使用某個對象時敛苇,這個對象所占用的內(nèi)存就會被自動釋放,還基于引用計數(shù)實現(xiàn)了對象共享機(jī)制顺呕,從而來實現(xiàn)節(jié)約內(nèi)存的目的枫攀。
Redis的對象還記錄了訪問時間,這個信息可以用于計算數(shù)據(jù)庫鍵的空轉(zhuǎn)時間株茶,空轉(zhuǎn)時間較長的那些對象有可能被服務(wù)器刪除掉来涨。

8.1 對象類型和編碼

Redis使用對象來表示數(shù)據(jù)庫中的鍵值對,每當(dāng)我們在數(shù)據(jù)庫中新創(chuàng)建一個鍵值對時启盛,我們至少會創(chuàng)建兩個對象蹦掐,一個鍵對象和一個值對象。Redis使用redisObject來表示對象:

typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). */
    int refcount;
    
    // 指向底層實現(xiàn)數(shù)據(jù)結(jié)構(gòu)的指針
    void *ptr;
} robj;

8.1.1 類型

對象的type屬性記錄了對象的類型僵闯,這個屬性值可以是下面幾種中的一個:

REDIS_STRING        :字符串對象
REDIS_LIST              :列表對象  
REDIS_HASH            :哈希對象
REDIS_SET               :集合對象
REDIS_ZSET             :有序集合對象

在Redis中卧抗,鍵值對的鍵總是一個字符串對象,而值可以是上面說到的五種對象之一鳖粟,可以使用TYPE命令來查看一個鍵值對的值的對象類型社裆。

8.1.2 編碼

編碼決定了底層實現(xiàn)的數(shù)據(jù)結(jié)構(gòu)∠蛲迹可以通過OBJECT ENCODING 命令來查看編碼信息泳秀;Redis沒有將一種類型關(guān)聯(lián)到一種特定類型的編碼上,極大的提升了Redis的靈活性和效率榄攀,Redis可以根據(jù)對象的特征進(jìn)行不同的編碼嗜傅,比如,在一個列表鍵包含較少的列表項時檩赢,Redis就會使用壓縮列表來實現(xiàn)吕嘀,而當(dāng)壓縮列表不再適應(yīng)的情況下,就會使用列表來實現(xiàn)。

8.2 字符串對象

字符串對象的編碼可以是int币他、raw或者embstr坞靶。

  • 如果一個字符串保存的是整數(shù)值,那么編碼就是int蝴悉;
  • 如果字符串對象保存的是一個字符串彰阴,并且字符串長度大于32字節(jié),那么就會使用SDS來表示拍冠,編碼為raw尿这;
  • 如果字符串對象保存的是一個長度小于等于32字節(jié)的字符串,那么就會使用embstr編碼來保存庆杜;
  • 如果保存的是浮點數(shù)射众,那么也是使用字符串的方式保存;

embstr編碼和raw的效果是一樣的晃财,但是embstr使用一次內(nèi)存分配/回收來管理RedisObject結(jié)構(gòu)和sdshdr結(jié)構(gòu)叨橱,而raw會調(diào)用兩次內(nèi)存分配。并且embstr的redisObject和sdshdr是連續(xù)分布的断盛,所以可以更好的利用緩存帶來的優(yōu)勢罗洗。

8.3 列表對象

列表對象的編碼可以是ziplist或者linkedlist。當(dāng)列表對象可以同時滿足下面兩個條件時钢猛,使用ziplist來保存:

  • 列表對象保存的所有字符串長度都小于64字節(jié)伙菜;
  • 列表對象保存的元素數(shù)量少于512個;
    如果不能滿足這兩個對象命迈,則使用linkedlist來實現(xiàn)贩绕;對于使用ziplist的列表對象,如果其中一個條件不再滿足的時候壶愤,就會進(jìn)行編碼轉(zhuǎn)換淑倾,轉(zhuǎn)換成使用linkedlist來進(jìn)行保存。

8.4 哈希對象

哈希對象的編碼可以是ziplist或者h(yuǎn)ashtable征椒,當(dāng)哈希對象可以同時滿足下面兩個條件時踊淳,使用ziplist,否則使用hashtab:

  • 哈希對象保存的所有鍵值對的鍵和值的字符串長度都小于64字節(jié)陕靠;
  • 哈希對象保存的鍵值對數(shù)量小于512個迂尝;

8.5 集合對象

集合對象的編碼可以是intset、hashtable剪芥,當(dāng)集合對象可以同時滿足下面兩個條件時垄开,使用intset,否則使用hashtabe:

  • 集合對象保存的所有元素都是整數(shù)值税肪;
  • 集合對象保存的元素數(shù)量不超過512個溉躲;

8.6 有序集合對象

有序集合對象的編碼可以是ziplist或者skiplist榜田,在Redis中,同時使用了跳躍表和字典來實現(xiàn)有序集合锻梳,zset的zsl屬性是一個跳躍表箭券,它按照元素的分值從小打到的保存元素,這樣就可以實現(xiàn)比如范圍操作之類的功能疑枯,而zset的dict屬性使用字段來存儲了集合的成員到分值的映射辩块,這樣基于成員查找分值的時間復(fù)雜度就是O(1),同時需要說明的是荆永,Redis使用對象共享技術(shù)废亭,所以同時使用跳躍表和字典不會造成內(nèi)存浪費。

同時使用跳躍表和字典來實現(xiàn)有序集合具钥,主要是為了實現(xiàn)效率上的考慮豆村,如果只使用字典來實現(xiàn),那么雖然根據(jù)成員查詢分值的時間復(fù)雜度是O(1)骂删,但是因為字典以無序的方式存儲元素掌动,所以在執(zhí)行范圍操作的時候,就需要先排序再操作宁玫,時間復(fù)雜度是O(NlogN)粗恢,還要創(chuàng)建一個額外的數(shù)組來存儲排序后的元素(O(N)),如果只使用跳躍表撬统,那么雖然范圍查找沒問題,但是基于成員查詢分值的操作就會從O(1)提升到O(N)敦迄,所以同時使用兩種數(shù)據(jù)結(jié)構(gòu)來實現(xiàn)恋追。

當(dāng)有序集合對象可以同時滿足下面兩個條件時,對象使用ziplist來實現(xiàn)罚屋,否則使用skiplist來實現(xiàn):

  • 有序集合保持的元素數(shù)量小于128個苦囱;
  • 有序集合保存的所有元素的長度小于64字節(jié);

8.8 內(nèi)存回收

Redis使用引用計數(shù)來實現(xiàn)對象跟蹤脾猛,對象的引用計數(shù)會隨著對象的使用而發(fā)生變化:

  • 在創(chuàng)建一個新對象時撕彤,引用計數(shù)的值會被初始化為1;
  • 當(dāng)一個對象被一個程序引用時猛拴,它的引用計數(shù)值會加1羹铅;
  • 當(dāng)對象不再被一個程序使用時,它的引用計數(shù)會減1愉昆;
  • 當(dāng)對象的引用計數(shù)變?yōu)?時职员,對象所占用的內(nèi)存會被釋放;

8.9 對象共享

當(dāng)A和B需要的對象一樣時跛溉,Redis就會使用對象共享技術(shù)焊切,不會創(chuàng)建多個對象扮授,而是會復(fù)用現(xiàn)有的對象。Redis只會共享包含整數(shù)值的字符串對象专肪,因為其他的字符串對象的對比需要消耗更多的CPU刹勃,得不償失;

8.10 對象的空轉(zhuǎn)時常

每個對象都會記錄對象最后一次被訪問的時間戳嚎尤,當(dāng)執(zhí)行命令OBJECT IDLETIME時荔仁,就是將當(dāng)前時間減去對象的這個時間戳,需要注意的是诺苹,這個命令執(zhí)行時咕晋,對象的lru(最后訪問時間戳)不會被更新∈毡迹空轉(zhuǎn)時間還可以用于Redis服務(wù)器淘汰鍵值對掌呜,如果開啟了maxmemory選項,并且服務(wù)器用于回收內(nèi)存的算法為valatile-lru或者allkeys-lru時坪哄,那么服務(wù)器占用的內(nèi)存數(shù)超過maxmemory時质蕉,就會將空轉(zhuǎn)時間較長的部分鍵值對優(yōu)先釋放內(nèi)存;

第九章 數(shù)據(jù)庫

Redis默認(rèn)情況下會初始化16個數(shù)據(jù)庫翩肌,每個數(shù)據(jù)庫由redisDb結(jié)構(gòu)表示模暗。
每個Redis客戶端都會有自己的目標(biāo)數(shù)據(jù)庫,在客戶端執(zhí)行讀寫數(shù)據(jù)庫的時候念祭,目標(biāo)數(shù)據(jù)庫就會成員操作對象兑宇。默認(rèn)情況下,目標(biāo)數(shù)據(jù)庫為0號數(shù)據(jù)庫粱坤,可以通過執(zhí)行SELECT命令來切換數(shù)據(jù)庫隶糕;在服務(wù)器內(nèi)部的redisClient結(jié)構(gòu)的db屬性記錄了客戶端當(dāng)前的目標(biāo)數(shù)據(jù)庫,這個屬性是指向redisDb結(jié)構(gòu)的指針站玄;

Redis的redisDb結(jié)構(gòu)里面有一個dict屬性枚驻,它記錄了數(shù)據(jù)庫中的所有鍵值對,我們將這個字段稱為鍵空間株旷。

Redis的redisDb結(jié)構(gòu)中的expires字典保存了數(shù)據(jù)庫中所有鍵的過期時間再登,我們稱這個字典為過期字典,當(dāng)執(zhí)行了為鍵值對設(shè)置過期時間的命令之后晾剖,就會在expires字典里面插入一個新的鍵值對锉矢,值為過期時間。當(dāng)為某個鍵移除過期時間時齿尽,相應(yīng)的也會在過期字典里面將其刪掉沈撞。

9.1 過期鍵刪除策略

如果一個鍵過期了,那么它被刪除的策略有三種:

  • 定時刪除:在設(shè)置鍵的過期時間的同時雕什,設(shè)置一個定時器缠俺,定時器在鍵的過期時間來臨時显晶,立即刪除鍵;
  • 惰性刪除:放任鍵過期不管壹士,但是每次從鍵空間中獲取鍵時磷雇,都檢查鍵是否已經(jīng)過期,如果過期的話躏救,則刪除鍵唯笙,否則返回;
  • 定期刪除:每隔一段時間盒使,就對數(shù)據(jù)庫進(jìn)行一次檢查崩掘,刪除里面的過期鍵,至于要刪除多少鍵少办,以及檢查多少個數(shù)據(jù)庫苞慢,則由算法決定;

定時刪除對內(nèi)存最優(yōu)化英妓,那些過期的鍵都能及時被清理挽放,而如果有大量的鍵需要清理則對CPU不太友好,需要大量的CPU時間蔓纠;惰性刪除對內(nèi)存不太友好辑畦,但是對CPU友好;定期刪除策略是對前兩種策略的折中方案:

  • 定期刪除策略每隔一段時間執(zhí)行一次過期鍵刪除操作腿倚,并通過限制刪除操作執(zhí)行的時常和頻率來減少刪除操作對CPU時間的影響纯出;
  • 除此之外,通過定期刪除過期鍵敷燎,可以減少過期鍵帶來的內(nèi)存浪費暂筝;

Redis實際使用的是惰性刪除和定期刪除兩種策略,服務(wù)器可以很好的在合理使用CPU和避免內(nèi)存空間浪費之間取得平衡懈叹;

9.1.1 惰性刪除策略的實現(xiàn)

過期鍵的惰性刪除由db.c/expireIfNeeded函數(shù)實現(xiàn)乖杠,所有讀寫Redis數(shù)據(jù)庫的命令在執(zhí)行之前都會調(diào)用這個函數(shù)進(jìn)行鍵過期檢查分扎;

  • 如果輸入鍵已經(jīng)過期澄成,那么函數(shù)將輸入鍵從數(shù)據(jù)庫中刪除;
  • 如果輸入鍵不過期畏吓,那么這個函數(shù)什么也不做墨状;

9.1.2 定期刪除策略的實現(xiàn)

定期刪除由redis.c/activeExpieCycle函數(shù)實現(xiàn),每當(dāng)Redis的服務(wù)器周期性的執(zhí)行serverCorn函數(shù)時菲饼,activeExpieCycle函數(shù)也會被調(diào)用肾砂,它在規(guī)定的時間內(nèi),分多次遍歷服務(wù)器中的各個數(shù)據(jù)庫宏悦,從數(shù)據(jù)庫的expires字典中隨機(jī)檢查一部分鍵的過期時間镐确,并刪除其中的過期鍵包吝。

9.2 AOF、RDB和復(fù)制功能對過期鍵的處理

9.2.1 生成RDB文件

在執(zhí)行SAVE或者BGSACE命令時源葫,程序會對數(shù)據(jù)庫中的鍵進(jìn)行檢查诗越,已經(jīng)過期的鍵不會被保存到數(shù)據(jù)庫中;

9.2.2 載入RDB文件

在啟動Redis服務(wù)器時息堂,如果服務(wù)器開啟了RDB功能嚷狞,那么服務(wù)器將對RDB文件進(jìn)行載入:

  • 如果服務(wù)器以主服務(wù)器模式運行,那么在載入RDB文件時荣堰,程序會對保存的鍵進(jìn)行檢查床未,未過期的鍵會被保存到數(shù)據(jù)庫中,而過期的鍵則會被忽略振坚。
  • 如果服務(wù)器以從服務(wù)器運行薇搁,那么在載入RDB文件時,文件中保存的所有鍵都會被載入到數(shù)據(jù)庫中屡拨,不過只酥,因為主服務(wù)器在進(jìn)行數(shù)據(jù)同步時,從服務(wù)器的數(shù)據(jù)庫就會被清空呀狼,所以一般來講對從服務(wù)器也沒有影響裂允。

9.2.3 AOF文件寫入

如果鍵過期被惰性刪除或者定期刪除了,那么會向AOF文件寫入一條DEL命令哥艇,來顯示的記錄該鍵已經(jīng)被刪除绝编;

9.2.4 復(fù)制

當(dāng)服務(wù)器運行在復(fù)制模式下時,從服務(wù)器的過期鍵刪除動作由主服務(wù)器控制:

  • 主服務(wù)器在刪除一個過期鍵之后貌踏,會顯示的向所有從服務(wù)器發(fā)送一條DEL命令十饥,告訴從服務(wù)器刪除這個過期鍵;
  • 從服務(wù)器沒有權(quán)利刪除過期鍵祖乳,所以從服務(wù)器在遇到過期鍵時不會刪除過期鍵逗堵,它會等待主服務(wù)器來控制自己。

第十章 RDB持久化

RDB文件會將當(dāng)前Redis數(shù)據(jù)庫中的所有鍵值對記錄到一個二進(jìn)制文件中眷昆,有兩個命令可以生成RDB文件蜒秤,SAVE和BGSAVE;

SAVE命令會阻塞服務(wù)器進(jìn)程亚斋,直到RDB文件生成完成作媚,BGSAVE則會派生出一個子進(jìn)程來負(fù)責(zé)生成RDB文件,而主進(jìn)程依然可以處理Redis的其他命令帅刊。RDB文件的載入沒有命令纸泡,是Redis在啟動的時候自動完成的,只要Redis檢測到RDB文件赖瞒,就會自動載入RDB文件到內(nèi)存女揭,需要注意的是蚤假,因為AOF文件的更新頻率更高,所以:

  • 如果服務(wù)器開啟了AOF持久化功能吧兔,那么服務(wù)器會優(yōu)先使用AOF文件來還原數(shù)據(jù)庫勤哗;
  • 只有在AOF功能處于關(guān)閉狀態(tài),服務(wù)器才會使用RDB文件來還原數(shù)據(jù)庫掩驱;

第十一章 AOF持久化

AOF和RDB不一樣芒划,AOF保存的是執(zhí)行過的命令,Redis會將所有執(zhí)行過寫命令追加到AOF文件中去欧穴,啟動Redis服務(wù)器的時候恢復(fù)這個數(shù)據(jù)庫;

AOF分為命令追加涮帘、文件寫入拼苍、文件同步三個部分;

11.1 命令追加

當(dāng)AOF持久化功能處于打開狀態(tài)時调缨,服務(wù)器在執(zhí)行完一個命令之后疮鲫,會按照協(xié)議格式將被執(zhí)行的命令追加到服務(wù)器狀態(tài)的aof_buf緩沖區(qū)的末尾;

11.2 文件寫入 與同步

Redis的服務(wù)器其實就是一個事件循環(huán)弦叶,包括文件循環(huán)和時間循環(huán)俊犯,服務(wù)器在處理文件事件時可能會往aof_buf里面寫入內(nèi)容,所以在每次結(jié)束一次循環(huán)之前伤哺,會調(diào)用flushAppendOnlyFile函數(shù)燕侠,考慮是否需要將aof_buf里面的內(nèi)容保存到AOF文件里面去。flushAppendOnlyFile函數(shù)的行為由appendfasync選項的值來決定:

  • always:將aof_buf里面的內(nèi)容寫入并同步到AOF文件立莉;
  • everysec:將aof_buf里面的內(nèi)容寫入到AOF文件绢彤,如果上次同步aof文件的時間距離現(xiàn)在超過1秒,那么再次對AOF文件進(jìn)行同步蜓耻,并且這個事情由一個專門的線程負(fù)責(zé)茫舶;
  • no:將aof_buf里面的內(nèi)存寫入到AOF文件,但并不對AOF文件進(jìn)行同步刹淌,何時同步由操作系統(tǒng)決定饶氏;

默認(rèn)為everysec。

11.3 AOF重寫

因為AOF持久化是通過保存redis執(zhí)行的命令來實現(xiàn)的芦鳍,隨著服務(wù)器運行嚷往,這個文件會越來越大葛账,甚至?xí)绊憆edis服務(wù)器柠衅,所以需要重寫AOF文件,重寫是通過讀取redis當(dāng)前數(shù)據(jù)庫狀態(tài)來實現(xiàn)的籍琳;比如原來一個列表是通過10次添加元素構(gòu)成的菲宴,重寫之后只需要一條命令即可贷祈,大大縮短了命令的數(shù)量。

AOF重寫需要執(zhí)行大量的寫入操作喝峦,所以這個函數(shù)的線程將長時間阻塞势誊,這就會影響redis服務(wù)器處理正常的請求,所以AOF重寫的任務(wù)被設(shè)計成一個后臺進(jìn)程來完成谣蠢;

不過粟耻,這里需要注意的是,子進(jìn)程在進(jìn)行AOF重寫的時候眉踱,主進(jìn)程還在繼續(xù)處理請求挤忙,而新的命令可能會對現(xiàn)有的數(shù)據(jù)庫狀態(tài)進(jìn)行變更,從而使得當(dāng)前的數(shù)據(jù)庫狀態(tài)和重寫后的AOF文件所保存的數(shù)據(jù)庫狀態(tài)不一致谈喳,為了解決這個問題册烈,Redis服務(wù)器設(shè)置了一個AOF重寫緩沖區(qū),這個緩存區(qū)在服務(wù)器創(chuàng)建子進(jìn)程之后開始使用婿禽,當(dāng)Redis服務(wù)器執(zhí)行完一個寫命令之后赏僧,它會同時將這個寫命令發(fā)送給AOF緩沖區(qū)和AOF重寫緩沖區(qū),當(dāng)子進(jìn)程完成AOF重寫工作之后扭倾,它會向父進(jìn)程發(fā)送一個信號淀零,父進(jìn)程在接收到這個信號之后,會進(jìn)程信號處理:

  • 將AOF重寫緩沖區(qū)中的內(nèi)容全部寫入到新的AOF文件中膛壹;
  • 對新的AOF文件進(jìn)行改名窑滞,原子的覆蓋原來的AOF文件;

第十二章 事件

Redis服務(wù)器是一個事件驅(qū)動的程序恢筝,服務(wù)器需要處理兩類事件:

  • 文件事件:也就是I/O事件哀卫,比如客戶端的連接上的讀寫請求;
  • 時間事件:某些操作(比如serverCorn)需要在給定的時間點運行撬槽,這就是時間事件此改;

12.1 文件事件

Redis基于Reactor模式開發(fā)了自己的事件處理器,這個處理器被稱為文件事件處理器(file event handler)

  • 文件事件處理器使用I/O多路復(fù)用技術(shù)侄柔,同時監(jiān)聽多個套接字共啃,并根據(jù)套接字目前執(zhí)行的任務(wù)設(shè)置不同的事件處理器;
  • 當(dāng)被監(jiān)聽的套接字上有讀寫事件發(fā)生時暂题,文件事件處理器就會調(diào)用相應(yīng)的處理器來處理這些事件移剪;

12.2 時間事件

服務(wù)器將所有的時間事件都放在一個無序列表里面,每當(dāng)時間事件執(zhí)行器運行時薪者,它就遍歷整個列表纵苛,查找所有已到達(dá)的時間事件,并調(diào)用相應(yīng)的時間處理器。一般情況下服務(wù)器只會執(zhí)行serverCorn一個時間事件攻人。

第十五章 復(fù)制

在Redis中取试,可以執(zhí)行SLAVEOF命令或者設(shè)置slaveof選項,讓一個服務(wù)器復(fù)制另外一個服務(wù)器怀吻,我們稱呼被復(fù)制的服務(wù)器為主服務(wù)器(master)瞬浓,而對主服務(wù)器進(jìn)行復(fù)制的則成為從服務(wù)器(slave)。

15.1 舊版本復(fù)制功能的實現(xiàn)(2.8版本之前)

Redis的復(fù)制分為同步(sync)和命令傳播(command propagate)兩個操作:

  • 同步:同步操作用于將從服務(wù)器的數(shù)據(jù)庫狀態(tài)更新至服務(wù)器所處的數(shù)據(jù)庫狀態(tài)蓬坡;
  • 命令傳播:命令傳播用于在主服務(wù)器的數(shù)據(jù)庫狀態(tài)被修改猿棉,導(dǎo)致主從服務(wù)器數(shù)據(jù)庫狀態(tài)不一致時,讓主從服務(wù)器的數(shù)據(jù)庫重新回到一致狀態(tài)屑咳;

15.1.1 同步(sync)

當(dāng)客戶端向從服務(wù)器發(fā)送SLAVEOF命令铺根,要求復(fù)制主服務(wù)器時,從服務(wù)器首先需要執(zhí)行同步操作乔宿,首先將從服務(wù)器的數(shù)據(jù)庫狀態(tài)更新到主服務(wù)器一致位迂。
從服務(wù)器通過向主服務(wù)器發(fā)送SYNC命令來完成同步操作,以下是SYNC命令的執(zhí)行步驟:

  • 從服務(wù)器向主服務(wù)器發(fā)送SYNC命令详瑞;
  • 收到SYNC命令的主服務(wù)器開始執(zhí)行BGSAVE命令掂林,在后臺生成一個RDB文件,并使用一個緩沖區(qū)記錄從現(xiàn)在開始執(zhí)行的所有寫命令坝橡;
  • 當(dāng)主服務(wù)器的BGSAVE命令執(zhí)行完成時涩拙,主服務(wù)器會將生成的RDB文件發(fā)送給從服務(wù)器嫂易,從服務(wù)器接收并載入這個RDB文件仁锯;
  • 主服務(wù)器將記錄在緩存區(qū)里面的所有寫命令發(fā)送給從服務(wù)器匙姜,從服務(wù)器執(zhí)行這些寫命令,將自己的狀態(tài)更新至主服務(wù)器當(dāng)前的狀態(tài)番宁。

15.1.2 命令傳播(command propagate)

當(dāng)同步操作完成之后元莫,主從服務(wù)器兩者的數(shù)據(jù)庫狀態(tài)將達(dá)到一致狀態(tài),但是這種狀態(tài)并不是一成不變的蝶押,如果主服務(wù)器的數(shù)據(jù)庫被改變踱蠢,則一致就不再存在。

為了讓從服務(wù)器和主服務(wù)器保持一致棋电,主服務(wù)器需要對從服務(wù)器執(zhí)行命令傳播茎截,主服務(wù)器會將自己執(zhí)行的寫命令,也就是會造成主從數(shù)據(jù)庫狀態(tài)不一致的命令赶盔,發(fā)送給從服務(wù)器執(zhí)行企锌,當(dāng)從服務(wù)器執(zhí)行之后,主從數(shù)據(jù)庫狀態(tài)又會變成一致于未。

15.2 舊版(2.8版本之前)復(fù)制功能的缺陷

在Redis中撕攒,從服務(wù)器對主服務(wù)器的復(fù)制可以分為以下兩種情況:

  • 初次復(fù)制:從服務(wù)器以前沒用復(fù)制過任何主服務(wù)器陡鹃,或者從服務(wù)器當(dāng)前要復(fù)制的主服務(wù)器和上一次復(fù)制的主服務(wù)器不同;
  • 斷線后重新復(fù)制:處于命令傳播階段的主從服務(wù)器因為網(wǎng)絡(luò)原因中斷了復(fù)制打却,但從服務(wù)器通過自動重連連接上了主服務(wù)器,并繼續(xù)復(fù)制主服務(wù)器谎倔;

對于初次復(fù)制來說柳击,舊版復(fù)制功能沒有問題,但是對于斷線重連后的復(fù)制片习,舊版雖然也可以讓主從服務(wù)器重新回到一致狀態(tài)捌肴,但是效率卻非常低。因為斷線重連后從服務(wù)器將重新進(jìn)行同步藕咏,執(zhí)行SYNC命令状知,這個命令是消除消耗資源的。

15.3 新版復(fù)制功能的實現(xiàn)

為了解決舊版復(fù)制功能在處理斷線重新復(fù)制情況下的低效問題孽查,從Redis 2.8開始饥悴,使用PSYNC命令進(jìn)行復(fù)制中的同步操作,PSYC命令有完整重同步和部分重同步兩種模式:

  • 完整重同步用于初次復(fù)制的情況:完整重同步和SYNC命令操作基本是一樣的盲再;
  • 部分重同步用于斷線后重復(fù)制的情況:當(dāng)從服務(wù)器斷線后重新連接到主服務(wù)器西设,如果條件允許,主服務(wù)器可以將斷線期間執(zhí)行過的寫命令發(fā)送給從服務(wù)器答朋,就可以將從服務(wù)器的數(shù)據(jù)庫更新成和主服務(wù)器一致贷揽;

15.4 部分重同步實現(xiàn)

部分重同步由三部分組成

  • 主服務(wù)器的復(fù)制偏移量和從服務(wù)器的復(fù)制偏移量(replication offset)
  • 主服務(wù)器的復(fù)制積壓緩沖區(qū)(replication backlog)
  • 服務(wù)器的運行ID(run ID)

15.4.1 復(fù)制偏移量

執(zhí)行復(fù)制的雙方會分別維護(hù)一個復(fù)制偏移量:

  • 主服務(wù)器每次向從服務(wù)器傳播N個字節(jié)的數(shù)據(jù)時,就將自己的復(fù)制偏移量加上N梦碗;
  • 從服務(wù)器每次收到主服務(wù)器傳播來的N字節(jié)數(shù)據(jù)時禽绪,就將自己的復(fù)制偏移量加上N;

通過對比主從服務(wù)器的復(fù)制偏移量洪规,可以很容易的發(fā)現(xiàn)主從數(shù)據(jù)庫是否一致:

15.4.2 復(fù)制積壓緩沖區(qū)

復(fù)制積壓緩沖區(qū)是主服務(wù)器維護(hù)的一個固定長度的FIFO隊列印屁,默認(rèn)大小為1Mb;當(dāng)主服務(wù)器在進(jìn)行命令傳播時斩例,它不僅會將寫命令發(fā)送給從服務(wù)器库车,還會將寫命令入隊到復(fù)制積壓緩沖區(qū)里面。

因此樱拴,復(fù)制積壓緩沖區(qū)會保持這一部分最近傳播的寫命令柠衍,并且賦值積壓緩沖區(qū)會為隊列中的每個字節(jié)記錄相應(yīng)的復(fù)制偏移量。當(dāng)斷線后從服務(wù)器重新連接上主服務(wù)器晶乔,從服務(wù)器會通過PSYNC命令將自己的復(fù)制偏移量發(fā)送給主服務(wù)器珍坊,主服務(wù)器會根據(jù)這個偏移量來決定執(zhí)行何種同步:

  • 如果offset偏移量之后的數(shù)據(jù)任然存在于復(fù)制積壓緩沖區(qū)里面,那么主服務(wù)器對從服務(wù)器執(zhí)行部分重同步操作正罢;
  • 否則阵漏,就會執(zhí)行完整重同步操作,這和SYNC命令是一樣的;

15.4.3 服務(wù)器運行ID

除了復(fù)制偏移量和復(fù)制積壓緩沖區(qū)履怯,還需要服務(wù)器運行ID回还,才能實現(xiàn)PSYNC部分重同步功能;

每個Redis服務(wù)器叹洲,無論是主服務(wù)器還是從服務(wù)器柠硕,都會有一個自己的運行ID;這個運行ID從服務(wù)器啟動時自動生成运提,由40個隨機(jī)的十六進(jìn)制字符組成蝗柔。

當(dāng)從服務(wù)器初次復(fù)制主服務(wù)器時,主服務(wù)器會將自己的運行ID傳送給從服務(wù)器民泵,而從服務(wù)器會將這個ID保存起來癣丧。當(dāng)從服務(wù)器斷線并重新連接上主服務(wù)器時,從服務(wù)器向當(dāng)前連接的主服務(wù)器發(fā)送這個ID:如果主服務(wù)器發(fā)現(xiàn)這個ID和自己一樣栈妆,那么可以執(zhí)行部分重同步功能胁编,否則說明這是一個新的從服務(wù)器,需要執(zhí)行完整重同步鳞尔。

15.5 復(fù)制的實現(xiàn)

  • 設(shè)置主服務(wù)器的地址和端口
  • 和主服務(wù)器建立套接字連接
  • 向主服務(wù)器發(fā)送PING命令
  • 主服務(wù)器進(jìn)行身份驗證
  • 從服務(wù)器向主服務(wù)器發(fā)送端口信息
  • 同步
  • 命令傳播
  • 心跳檢測(每秒一次)
    • 檢測主從服務(wù)器的網(wǎng)絡(luò)連接狀態(tài)
    • 輔助實現(xiàn)min-slaves配置掏呼,就是最少需要幾個slave(當(dāng)然還可以配置如果所有的slave的延時都大于多少時)拒絕執(zhí)行寫命令;
    • 檢測命令丟失铅檩,在網(wǎng)絡(luò)傳輸中命令傳播可能丟失憎夷,通過offset主服務(wù)器就會發(fā)現(xiàn)這種問題,將數(shù)據(jù)重傳昧旨;

第十六章 Sentinel

Sentinel(哨兵)是Redis高可用解決方案拾给,由一個或多個Sentinel實例組成的Sentinel系統(tǒng)可以監(jiān)視任意多個主服務(wù)器,以及這些主服務(wù)器的從服務(wù)器兔沃,當(dāng)監(jiān)視的主服務(wù)器下線時蒋得,自動將下線主服務(wù)器屬下的某個從服務(wù)器升級為新的主服務(wù)器。

當(dāng)主服務(wù)器的下線時長超過用戶設(shè)定的閾值時乒疏,Sentinel系統(tǒng)就會對主服務(wù)器進(jìn)行故障轉(zhuǎn)移操作:

  • 首先额衙,Sentinel會挑選出一個故障主服務(wù)器下面的其中一個從服務(wù)器,并將這個從服務(wù)器升級為主服務(wù)器怕吴;
  • 之后窍侧,Sentinel系統(tǒng)會向故障主服務(wù)器的所有從服務(wù)器發(fā)送新的復(fù)制命令,讓他們成為新的主服務(wù)器的從服務(wù)器转绷,當(dāng)所有的從服務(wù)器都開始復(fù)制新的主服務(wù)器時伟件,故障轉(zhuǎn)移操作完畢;
  • 另外议经,Sentinel系統(tǒng)還會繼續(xù)監(jiān)視已下線的服務(wù)器斧账,如果它重新上線谴返,那么會讓他成為新的主服務(wù)器的從服務(wù)器;

16.1 啟動并初始化Sentinel

當(dāng)一個Sentinel啟動時咧织,他需要執(zhí)行以下步驟:

  • 初始化服務(wù)器

Sentinel本質(zhì)上只是一個運行在特殊模式下的Redis服務(wù)器嗓袱,所以啟動Sentinel的第一步,就是初始化一個普通的Redis服務(wù)器习绢,但是不需要像普通Redis服務(wù)器那樣載入RDB文件之類的操作渠抹,因為它不需要使用Redis數(shù)據(jù)庫功能;

  • 將普通Redis服務(wù)器使用的代碼替換成Sentinel專用的代碼

  • 初始化Sentinel狀態(tài)

  • 根據(jù)給定的配置文件毯炮,初始化Sentinel的監(jiān)視主服務(wù)器列表

  • 創(chuàng)建連向主服務(wù)器的網(wǎng)絡(luò)連接

16.2 獲取主服務(wù)器信息

Sentinel默認(rèn)會以十秒一次的頻率通過命令連接向被監(jiān)視的主服務(wù)器發(fā)送INFO命令逼肯,通過分析主服務(wù)器返回的INFO命令回復(fù)耸黑,Sentinel可以獲取以下兩方面的信息:

  • 一方面是關(guān)于主服務(wù)器本身的信息桃煎,比如runID;
  • 另一方面是關(guān)于該主服務(wù)器下的從服務(wù)器的信息大刊,根據(jù)這些信息为迈,Sentinel就可以自動發(fā)現(xiàn)從服務(wù)器;

16.3 獲取從服務(wù)器信息

Sentinel默認(rèn)也會以十秒一次的頻率通過命令通道向從服務(wù)器發(fā)送INFO命令缺菌,獲取從服務(wù)器的信息葫辐。

16.4 向主服務(wù)器和從服務(wù)器發(fā)送信息

在默認(rèn)情況下,Sentinel會以每兩秒一次的頻率伴郁,通過命令連接耿战,向所有被監(jiān)視的Redis服務(wù)器發(fā)送一個命令,向服務(wù)器的sentinel:hello頻道發(fā)送一條信息焊傅;

16.5 接收來自被監(jiān)視服務(wù)器的頻道信息

Sentinel不僅會向所有被監(jiān)視的Redis服務(wù)器發(fā)送頻道信息剂陡,還會接收頻道信息,如果一個Redis服務(wù)器被多個Sentinel實例監(jiān)視狐胎,那么一個Sentinel向某個被監(jiān)視的Redis服務(wù)器發(fā)送的頻道信息鸭栖,會被其他所有監(jiān)視這個Redis服務(wù)器的Sentinel實例接收到,這些Sentinel接收到不是自己發(fā)送的頻道信息之后握巢,會對其他Sentinel發(fā)送的頻道信息進(jìn)行解析晕鹊;

16.5.1 更新sentinels字典

Sentinel為主服務(wù)器創(chuàng)建的sentinels字段保存了所有監(jiān)視這個Redis服務(wù)器的Sentinel的資料:

  • sentinels字典的鍵是Sentinel的名字,格式為ip:port
  • sentinels字典的值是對應(yīng)的Sentinel實例結(jié)構(gòu)

16.5.2 創(chuàng)建連向其他Sentinel的命令連接

當(dāng)Sentinel通過頻道信息發(fā)現(xiàn)一個新的Sentinel時暴浦,不僅會在sentinels 字典中未該Sentinel創(chuàng)建相應(yīng)的實例結(jié)構(gòu)溅话,還會創(chuàng)建一個連接到這個Sentinel的命令連接,最終監(jiān)視同一個Redis服務(wù)器的所有Sentinel實例會成為一個網(wǎng)狀結(jié)構(gòu)歌焦;

16.6 檢測主觀下線狀態(tài)

默認(rèn)情況下公荧,Sentinel會以每秒一次的頻率向所有它創(chuàng)建了命令連接的實例(包括主從服務(wù)器,其他Sentinel)發(fā)送PING命令同规,并通過實例返回來判斷實例是否在線循狰;

在Sentinel配置文件中有一個配置:down-after-milliseconeds窟社,如果一個實例在down-after-milliseconeds毫秒后依然沒有返回有效的PING回應(yīng),那么Sentinel就會判斷該實例為下線绪钥;

16.7 檢測客觀下線狀態(tài)

當(dāng)Sentinel將一個主服務(wù)器判斷為主觀下線后灿里,為了確認(rèn)這個服務(wù)器是否真的下線,它會詢問所有監(jiān)視這個服務(wù)器的Sentinel程腹,看看其他Sentinel是否也認(rèn)為這個服務(wù)器已經(jīng)下線匣吊,如果從其他Sentinel那里得到了足夠數(shù)量的下線判斷后(這個數(shù)量是配置的),Sentinel就會認(rèn)為服務(wù)器為客觀下線寸潦,并會對該主服務(wù)器進(jìn)行故障轉(zhuǎn)移操作色鸳。不同的Sentinel對主服務(wù)器的下線判斷可能不一樣,這個是因為不同的Sentinel配置可能是不一樣的见转。

16.8 選舉領(lǐng)頭Sentinel

當(dāng)一個主服務(wù)器被判定為客觀下線時命雀,監(jiān)視這個下線主服務(wù)器的各個Sentinel會進(jìn)行協(xié)商,選舉出一個領(lǐng)頭Sentinel斩箫,負(fù)責(zé)執(zhí)行故障轉(zhuǎn)移操作:

  • 所有在線的Sentinel都有資格成為領(lǐng)頭Sentinel吏砂;
  • 每次選舉之后,不論選舉是否成功乘客,所有Sentinel的紀(jì)元都會增加1(一個計數(shù)器)
  • 每個發(fā)現(xiàn)主服務(wù)器進(jìn)入客觀下線的Sentinel都會要求別人選舉自己成為領(lǐng)頭
  • 最想向目標(biāo)Sentinel發(fā)送選舉自己要求的Sentinel將獲得選舉狐血,其他后來的選舉要求將被目標(biāo)Sentinel拒絕;
  • 如果某個Sentinel被半數(shù)以上的Sentinel設(shè)置為leader易核,那么這個Sentinel將成為leader匈织;
  • 如果在給定的時間內(nèi)沒有選舉出一個領(lǐng)頭Sentinel,那么就會過一段時間再繼續(xù)選舉牡直,直到產(chǎn)生leader缀匕;

16.9 故障轉(zhuǎn)移

在選舉產(chǎn)生領(lǐng)頭Sentinel之后,領(lǐng)頭Sentinel就會對已下線的主服務(wù)器執(zhí)行故障轉(zhuǎn)移操作:

  • 在已下線的主服務(wù)器的所有從服務(wù)器里井氢,挑選出一個從服務(wù)器弦追,并將其轉(zhuǎn)換為新的主服務(wù)器;
  • 讓已下線的主服務(wù)器的所有從服務(wù)器改為復(fù)制新的主服務(wù)器花竞;
  • 將已下線服務(wù)器作為新的主服務(wù)器的從服務(wù)器劲件,當(dāng)這個舊的主服務(wù)器開始重新上線時,他就會成為新的主服務(wù)器的從服務(wù)器约急;

16.9.1 選出新的主服務(wù)器

故障轉(zhuǎn)移的第一步就是需要在故障的主服務(wù)器的所有從服務(wù)器中挑選一個狀態(tài)良好零远、數(shù)據(jù)完整的從服務(wù)器,然后向這個從服務(wù)器發(fā)送SLAVEOF no one命令厌蔽,將這個從服務(wù)器轉(zhuǎn)換成主服務(wù)器牵辣;

領(lǐng)頭Sentinel會將已下線的Sentinel服務(wù)器的所有從服務(wù)器保存在一個列表里面,然后按照下面的規(guī)則過濾:

  • 刪除列表中出于下線或者斷線狀態(tài)的服務(wù)器奴饮,保證剩余的從服務(wù)器都是正常在線的纬向;
  • 刪除列表中所有最近五秒內(nèi)沒有回復(fù)過領(lǐng)頭Sentinel的INFO命令的從服務(wù)器择浊,保證剩余的從服務(wù)器都是最近成功進(jìn)行過通信的;
  • 刪除所有與已下線服務(wù)器連接斷開超過 down-after-milliseconds * 10 毫秒的從服務(wù)器逾条,保證剩余的從服務(wù)器沒有過早的與主服務(wù)器斷開連接琢岩,也就是保證剩余的從服務(wù)器保存的數(shù)據(jù)都是比較新的;

之后师脂,領(lǐng)頭Sentinel對列表中剩余的從服務(wù)器進(jìn)行優(yōu)先級處理担孔,取得優(yōu)先級最高的一個從服務(wù)器,稱為最終被挑選出來的從服務(wù)器吃警。

排序的規(guī)則是:

首先看服務(wù)器優(yōu)先級糕篇,然后是復(fù)制偏移量,最后是服務(wù)器運行ID酌心;

16.9.2 修改從服務(wù)器的復(fù)制目標(biāo)

16.9.3 將舊的主服務(wù)器變?yōu)閺姆?wù)器

最后拌消,需要注意的是,Sentinel只會和主服務(wù)器和從服務(wù)器創(chuàng)建命令連接和訂閱連接谒府,Sentinel之間則只創(chuàng)建命令連接拼坎。

第十七章 集群

Redis集群是Redis提供的分布式解決方案浮毯,復(fù)制(replication)提供了主從數(shù)據(jù)庫方案完疫,Sentinel提供了高可用(故障轉(zhuǎn)移)方案,而Redis集群則在此基礎(chǔ)上提供了負(fù)載均衡(通過分片)的分布式方案债蓝。

集群主要包括下面一些內(nèi)容:節(jié)點壳鹤、槽指派、命令執(zhí)行饰迹、重新分片芳誓、轉(zhuǎn)向、故障轉(zhuǎn)移啊鸭、消息等锹淌;

17.1 節(jié)點

一個Redis集群由多個節(jié)點組成(node),在剛開始的時候赠制,每個節(jié)點都是相互獨立的赂摆,他們都處于一個只包含自己的集群當(dāng)中,我們可以通過CLUSTER MEET <ip> <port> 命令來完成這個工作钟些,當(dāng)向一個節(jié)點執(zhí)行這個命令時烟号,可以讓當(dāng)前節(jié)點將ip和port所代表的節(jié)點添加到當(dāng)前集群中;

一個節(jié)點就是一個運行在集群模式的Redis服務(wù)器政恍,Redis服務(wù)器會在啟動的時候根據(jù)cluster-enbale配置是否為yes來確定是否開啟集群模式汪拥;集群模式的Redis服務(wù)器依然會繼續(xù)使用單機(jī)模式下的Redis服務(wù)器組件,比如復(fù)制等功能篙耗;

clusterNode結(jié)構(gòu)是一個節(jié)點的數(shù)據(jù)結(jié)構(gòu):

typedef struct clusterNode {
    mstime_t ctime; /* Node object creation time. */
    char name[CLUSTER_NAMELEN]; /* Node name, hex string, sha1-size */
    int flags;      /* CLUSTER_NODE_... */
    uint64_t configEpoch; /* Last configEpoch observed for this node */
    unsigned char slots[CLUSTER_SLOTS/8]; /* slots handled by this node */
    int numslots;   /* Number of slots handled by this node */
    int numslaves;  /* Number of slave nodes, if this is a master */
    struct clusterNode **slaves; /* pointers to slave nodes */
    struct clusterNode *slaveof; /* pointer to the master node. Note that it
                                    may be NULL even if the node is a slave
                                    if we don't have the master node in our
                                    tables. */
    mstime_t ping_sent;      /* Unix time we sent latest ping */
    mstime_t pong_received;  /* Unix time we received the pong */
    mstime_t fail_time;      /* Unix time when FAIL flag was set */
    mstime_t voted_time;     /* Last time we voted for a slave of this master */
    mstime_t repl_offset_time;  /* Unix time we received offset for this node */
    mstime_t orphaned_time;     /* Starting time of orphaned master condition */
    long long repl_offset;      /* Last known repl offset for this node. */
    char ip[NET_IP_STR_LEN];  /* Latest known IP address of this node */
    int port;                   /* Latest known clients port of this node */
    int cport;                  /* Latest known cluster port of this node. */
    clusterLink *link;          /* TCP/IP link with this node */
    list *fail_reports;         /* List of nodes signaling this as failing */
} clusterNode;

每個節(jié)點都保存這一個clusterState:

typedef struct clusterState {
    clusterNode *myself;  /* This node */
    uint64_t currentEpoch;
    int state;            /* CLUSTER_OK, CLUSTER_FAIL, ... */
    int size;             /* Num of master nodes with at least one slot */
    dict *nodes;          /* Hash table of name -> clusterNode structures */
    dict *nodes_black_list; /* Nodes we don't re-add for a few seconds. */
    clusterNode *migrating_slots_to[CLUSTER_SLOTS];
    clusterNode *importing_slots_from[CLUSTER_SLOTS];
    clusterNode *slots[CLUSTER_SLOTS];
    uint64_t slots_keys_count[CLUSTER_SLOTS];
    rax *slots_to_keys;
    /* The following fields are used to take the slave state on elections. */
    mstime_t failover_auth_time; /* Time of previous or next election. */
    int failover_auth_count;    /* Number of votes received so far. */
    int failover_auth_sent;     /* True if we already asked for votes. */
    int failover_auth_rank;     /* This slave rank for current auth request. */
    uint64_t failover_auth_epoch; /* Epoch of the current election. */
    int cant_failover_reason;   /* Why a slave is currently not able to
                                   failover. See the CANT_FAILOVER_* macros. */
    /* Manual failover state in common. */
    mstime_t mf_end;            /* Manual failover time limit (ms unixtime).
                                   It is zero if there is no MF in progress. */
    /* Manual failover state of master. */
    clusterNode *mf_slave;      /* Slave performing the manual failover. */
    /* Manual failover state of slave. */
    long long mf_master_offset; /* Master offset the slave needs to start MF
                                   or zero if stil not received. */
    int mf_can_start;           /* If non-zero signal that the manual failover
                                   can start requesting masters vote. */
    /* The followign fields are used by masters to take state on elections. */
    uint64_t lastVoteEpoch;     /* Epoch of the last vote granted. */
    int todo_before_sleep; /* Things to do in clusterBeforeSleep(). */
    /* Messages received and sent by type. */
    long long stats_bus_messages_sent[CLUSTERMSG_TYPE_COUNT];
    long long stats_bus_messages_received[CLUSTERMSG_TYPE_COUNT];
    long long stats_pfail_nodes;    /* Number of nodes in PFAIL status,
                                       excluding nodes without address. */
} clusterState;

這個結(jié)構(gòu)記錄著當(dāng)前節(jié)點的視角下集群目前所處的狀態(tài)迫筑。

17.1.1 CLUSTER MEET命令的實現(xiàn)

通過向節(jié)點A發(fā)送CLUSTER MEET命令宪赶,可以讓節(jié)點A將另外一個節(jié)點B添加到節(jié)點A當(dāng)前所在的集群里面,收到命令的節(jié)點A將于節(jié)點B進(jìn)行握手脯燃,以此來確認(rèn)彼此的存在:

  • (1) 節(jié)點A會為節(jié)點B創(chuàng)建一個clusterNode結(jié)構(gòu)逊朽,并將該結(jié)構(gòu)添加到自己的clusterState.nodes字典里面;
  • (2)之后曲伊,節(jié)點A將給節(jié)點B發(fā)送一條MEET消息叽讳;
  • (3)節(jié)點B接收到節(jié)點A發(fā)送的MEET消息,B會為A創(chuàng)建一個clusterNode結(jié)構(gòu)坟募,并將該結(jié)構(gòu)添加到自己的clusterState.nodes字典里面岛蚤;
  • (4)之后,節(jié)點B將給節(jié)點A返回一條PONG消息懈糯;
  • (5)節(jié)點A收到B的PONG消息涤妒,A就知道B已經(jīng)成功的接收到自己的消息了;
  • (6)之后赚哗,節(jié)點A將給B發(fā)送一條PING消息她紫;
  • (7)節(jié)點B收到A的PING消息,B就知道A已經(jīng)接收到自返回的PONG消息屿储,握手結(jié)束贿讹;
  • (8)之后,節(jié)點A會將B的信息通過Gossip協(xié)議傳播給集群中的其他節(jié)點够掠,讓其他節(jié)點也與B握手民褂,最終,經(jīng)過一段時間疯潭,節(jié)點B會被集群中的所有節(jié)點認(rèn)識相叁;

17.2 槽指派

Redis集群通過分片的方式來保存數(shù)據(jù)庫中的鍵值對赎离,集群的整個數(shù)據(jù)庫被分為16384(2^14)個槽(slot)荣病,數(shù)據(jù)庫中的每個鍵都屬于這16384個槽中的一個,集群中的每個節(jié)點可以處理0個或最多16384個槽雹有;
當(dāng)數(shù)據(jù)庫中的16384個槽都被處理時合武,集群狀態(tài)出于上線狀態(tài)(ok),如果數(shù)據(jù)庫中任意一個槽沒有被處理在旱,那么集群出于下線狀態(tài)(fail)噪服;

可以通過命令 CLUSTER ADDSLOTS命令將一個或者多個槽指派給某個節(jié)點負(fù)責(zé);

節(jié)點的clusterNode結(jié)構(gòu)的slots 屬性和 numslots 記錄了節(jié)點負(fù)責(zé)處理那些槽带到,slots屬性是一個二進(jìn)制位數(shù)組,這個數(shù)組的長度是16384 / 8 = 2048個字節(jié)英染,一共包含16384個二進(jìn)制位揽惹。

Redis以0為起始索引,16383為終止索引四康,對slots數(shù)組中的16384個二進(jìn)制位進(jìn)行編號搪搏,并根據(jù)第i位上的二進(jìn)制值來判斷槽是否是該節(jié)點負(fù)責(zé);

一個節(jié)點會將自己負(fù)責(zé)的槽信息發(fā)送給其他集群中的節(jié)點闪金,以此來告訴其他節(jié)點自己負(fù)責(zé)哪些槽疯溺;節(jié)點A通過消息接收到節(jié)點B負(fù)責(zé)的槽信息,會在A節(jié)點自己的clusterState.nodes字典里面找到B對應(yīng)的clusterNode結(jié)構(gòu)哎垦,并對其中的slots屬性進(jìn)行更新囱嫩,因為集群中的每個節(jié)點都會將自己的slots數(shù)組發(fā)送給其他節(jié)點,所以集群中的所有節(jié)點都知道16384個槽都是由哪些節(jié)點負(fù)責(zé)的漏设;

這里需要注意的一點是墨闲,如果只將16384個槽記錄在clusterNode的slots數(shù)組里,那么如果需要知道第i個槽是哪個節(jié)點負(fù)責(zé)的郑口,就需要遍歷clusterState.nodes字典中的所有clusterNode.slots數(shù)組鸳碧,這個負(fù)責(zé)度為O(N),所以犬性,clusterState.slots數(shù)組會保存每一個槽是由哪個節(jié)點負(fù)責(zé)的瞻离。這樣的話,如果想要檢測槽i是否已經(jīng)被指派乒裆,只需要檢測clusterState.slots數(shù)組的第i個項即可套利;
當(dāng)然,是否只需要在clusterState.slots數(shù)組就可以了呢?clusterNode.slots是否還需要呢日裙?答案是需要的吹艇,因為節(jié)點需要將自己負(fù)責(zé)的slots傳播給其他節(jié)點,現(xiàn)在只需要將clusterNode.slots發(fā)送出去即可昂拂,但是如果沒有clusterNode.slots受神,就需要在clusterStste.slots里面遍歷,找出所有某個節(jié)點負(fù)責(zé)的slot格侯,然后再發(fā)送給其他節(jié)點鼻听,這就有點麻煩了;

17.3 在集群中執(zhí)行命令

在對集群中的16384個槽都指派完成之后联四,集群就進(jìn)入上線狀態(tài)撑碴,這時客戶端就可以向集群發(fā)送數(shù)據(jù)命令了;

當(dāng)客戶端向節(jié)點發(fā)送數(shù)據(jù)庫相關(guān)命令時朝墩,接收到命令的節(jié)點會計算出命令要處理的數(shù)據(jù)庫鍵屬于哪個槽:

  • 如果這個槽是自己負(fù)責(zé)的醉拓,那么節(jié)點直接執(zhí)行這個命令;
  • 如果鍵所在的槽不是當(dāng)前節(jié)點負(fù)責(zé)的收苏,那么節(jié)點會向客戶端返回一個MOVED錯誤亿卤,指引客戶端轉(zhuǎn)向正確的節(jié)點執(zhí)行命令;

節(jié)點首先需要計算出鍵所對應(yīng)的槽鹿霸,Redis使用CRC16(key) & 16383來計算這個值排吴,當(dāng)節(jié)點計算出鍵所對應(yīng)的槽之后,節(jié)點就會檢查自己在數(shù)組clusterState.slots的第i項是否是自己懦鼠,如果是钻哩,則說明這個鍵是由自己負(fù)責(zé)的,否則肛冶,會指引客戶端轉(zhuǎn)向clusterState.slots[i]所指向的節(jié)點執(zhí)行命令街氢;

需要注意的是,節(jié)點只能使用0號數(shù)據(jù)庫淑趾,而單機(jī)則沒有這個限制阳仔;

17.4 重新分片

Redis集群的重新分片操作可以將任意已經(jīng)分配給某個節(jié)點的槽改為指派給其他的另外一個節(jié)點,重新分片操作可以在線進(jìn)行扣泊,不需要下線,源節(jié)點和目標(biāo)節(jié)點都可以繼續(xù)處理命令請求嘶摊;

Redis集群的重新分片操作由Redis的集群管理軟件redis-trib執(zhí)行延蟹,redis-trib對集群的單個槽slot進(jìn)行重新分片的步驟如下:

  • (1)redis-trib向目標(biāo)節(jié)點發(fā)送CLUSTER SETSLOT <slot> IMPORTING <source_id>命令,讓目標(biāo)節(jié)點準(zhǔn)備好從源節(jié)點導(dǎo)入屬于槽slot的鍵值對叶堆;
  • (2)redis-trib對源節(jié)點發(fā)送CLUSTER SETSLOT <slot> MIGRATING <target_id>命令阱飘,讓源節(jié)點準(zhǔn)備好將屬于槽slot的鍵值對遷移到目標(biāo)節(jié)點;
  • (3)redis-trib向源節(jié)點發(fā)送CLUSTER GETKEYSINSLOT <slot> <count>命令,獲取最多count個屬于槽slot的鍵值對的鍵名字沥匈;
  • (4)對于步驟3得到的每個鍵蔗喂,redis-trib都將向源節(jié)點發(fā)送命令,原子的將這些鍵遷移到目標(biāo)節(jié)點高帖;
  • (5)重復(fù)步驟3和4缰儿,直到源節(jié)點保存的所有屬于槽slot的鍵值對都被遷移到目標(biāo)節(jié)點為止;
  • (6)redis-trib會向集群中的任意一個節(jié)點發(fā)送CLUSTER SETSLOT <slot> NODE <taregt_id>命令散址,將槽slot指派給目標(biāo)節(jié)點乖阵,這一信息會通過消息發(fā)送給集群中的所有節(jié)點,最終集群中的所有節(jié)點都將知道槽slot已經(jīng)指派給了目標(biāo)節(jié)點预麸;

17.5 ASK錯誤

在進(jìn)行重新分片的過程中瞪浸,因為集群正常在線,所以可能出現(xiàn)一種情況吏祸,屬于被遷移槽的一部分鍵在源節(jié)點中对蒲,而另一部分已經(jīng)遷移到目標(biāo)節(jié)點,這時候如果客戶端向源節(jié)點發(fā)送一個數(shù)據(jù)庫操作命令贡翘,而這個被操作的鍵剛好在被遷移的鍵中時:

  • 源節(jié)點會現(xiàn)在自己的數(shù)據(jù)庫中查找給定的鍵齐蔽,如果找到了,那么就直接處理床估;
  • 如果沒找到含滴,那么源節(jié)點會向客戶端返回一個ASK錯誤,指引客戶端向目標(biāo)節(jié)點發(fā)起請求丐巫;

ASK錯誤和MOVED錯誤的區(qū)別:

  • MOVED錯誤表示槽的負(fù)責(zé)權(quán)已經(jīng)從一個節(jié)點轉(zhuǎn)向另外一個節(jié)點了谈况,所以在客戶端收到MOVED錯誤之后,客戶端每次都可以使用轉(zhuǎn)向后的節(jié)點递胧;
  • ASK錯誤是一種臨時方案碑韵,是一次性的,ASK錯誤之后應(yīng)該會出現(xiàn)一次MOVED錯誤缎脾;

17.6 復(fù)制與故障轉(zhuǎn)移

Redis集群的節(jié)點分為主節(jié)點(master)和從節(jié)點(slave)祝闻,主節(jié)點負(fù)責(zé)處理槽,而從節(jié)點負(fù)責(zé)復(fù)制某個主節(jié)點遗菠,并在主節(jié)點下線時代替主節(jié)點联喘;

向一個節(jié)點發(fā)送CLUSTER REPLICATION <node_id>,可以讓收到命令的節(jié)點對主節(jié)點進(jìn)行復(fù)制:

  • 接收到命令的節(jié)點首先會在自己的clusterState.modes字典中找到主節(jié)點的clusterNode結(jié)構(gòu)辙纬,然后將自己的clusterState.myself.slaveof指針指向這個clusterNode豁遭,以此來表示當(dāng)前節(jié)點正在復(fù)制這個主節(jié)點;
  • 然后會修改自己的clusterState.myself.flags中的屬性贺拣,關(guān)閉原本的master屬性蓖谢,打開slave屬性捂蕴,表示節(jié)點變成一個從節(jié)點;
  • 然后闪幽,會復(fù)用復(fù)制的代碼對master節(jié)點進(jìn)行復(fù)制啥辨;

一個節(jié)點成為從節(jié)點,并開始復(fù)制某個主節(jié)點這個消息會被集群的其他節(jié)點知道盯腌,集群中的所有節(jié)點都會在代表主節(jié)點的clusterNode.slaves屬性中記錄正在復(fù)制該節(jié)點的從節(jié)點信息溉知;

集群中的每個節(jié)點都會定期向其他節(jié)點發(fā)送PING消息,以此來檢測對方是都還在線腊嗡,如果接收PING消息的節(jié)點沒有在規(guī)定時間內(nèi)返回PONG消息着倾,那么發(fā)送PING消息的節(jié)點就會將接受PING消息的節(jié)點標(biāo)記為疑似下線(probable fail ,PFAIL)燕少;
集群中的每個節(jié)點都會通過互相發(fā)送消息來交互集群各個節(jié)點的狀態(tài)信息卡者,如果一個集群里面超過半數(shù)以上負(fù)責(zé)處理槽的主節(jié)點將某個節(jié)點標(biāo)記為疑似下線狀態(tài),那么這個主節(jié)點將被標(biāo)記為下線客们,將這個主節(jié)點標(biāo)記為下線的節(jié)點將會向集群廣播這個消息崇决,所有收到這條消息的節(jié)點都會立即標(biāo)記這個節(jié)點已下線。

當(dāng)一個從節(jié)點發(fā)現(xiàn)自己復(fù)制的主節(jié)點已下線時底挫,從節(jié)點開始對下線主節(jié)點進(jìn)行故障轉(zhuǎn)移操作:

  • (1)在所有復(fù)制主節(jié)點的從節(jié)點里面會有一個從節(jié)點被選中恒傻;
  • (2)選中的從節(jié)點執(zhí)行SLAVE no one命令,成為新的主節(jié)點建邓;
  • (3)新的主節(jié)點會認(rèn)領(lǐng)所有下線主節(jié)點的槽指派盈厘;
  • (4)新的主節(jié)點向集群廣播一條PONG消息,集群中的其他節(jié)點會基于這條消息知道這個節(jié)點成為了新的主節(jié)點官边,并接管了原來主節(jié)點下的所有從節(jié)點沸手;
  • (5)新的主節(jié)點開始處理自己負(fù)責(zé)的槽的相關(guān)命令,故障轉(zhuǎn)移完成注簿;

選舉新的主節(jié)點的步驟為:

  • (1)在每次選舉中契吉,負(fù)責(zé)處理槽的主節(jié)點有一次投票機(jī)會;
  • (2)當(dāng)從節(jié)點發(fā)現(xiàn)自己正在復(fù)制的主節(jié)點已下線時诡渴,它會向集群廣播一條消息捐晶,要求所有具備選舉權(quán)的節(jié)點給自己投票;
  • (3)接收到這條消息的節(jié)點妄辩,如果是負(fù)責(zé)處理槽的主節(jié)點惑灵,并且自己還沒有投票,那么就會給這個節(jié)點投票恩袱;
  • (4)如果一個從節(jié)點收集到大于集群主節(jié)點數(shù)量一半的票數(shù)泣棋,那么這個從節(jié)點就選舉為新的主節(jié)點;
  • (5)如果一個紀(jì)元里面沒有從節(jié)點獲得的票數(shù)符合要求畔塔,那么就會進(jìn)入下一個紀(jì)元;

17.7 消息

消息類型:MEET PING PONG FAIL PUBLISH

gossip
redis cluster something

第十九章 事務(wù)

Redis通過MULTI、EXEC澈吨、WATCH等命令實現(xiàn)事務(wù)功能把敢,事務(wù)提供了一種將多個命令請求打包,然后一次性的谅辣,按順序的執(zhí)行多個命令的機(jī)制修赞,并且在事務(wù)執(zhí)行期間,服務(wù)器不會中斷事務(wù)而去執(zhí)行其他客戶端的請求桑阶,它會將事務(wù)中的命令都執(zhí)行完成柏副,然后才去執(zhí)行其他客戶端的命令;

19.1 事務(wù)

一個事務(wù)從開始到結(jié)束通常會經(jīng)歷以下三個階段:

  • 事務(wù)開始

MULTI命令可以讓執(zhí)行該命令的客戶端從非事務(wù)狀態(tài)切換到事務(wù)狀態(tài)蚣录,這一切換是通過在客戶端狀態(tài)的flags屬性打開REDIS_MULTI標(biāo)志來完成割择;

  • 命令入隊

如果一個客戶端處于非事務(wù)狀態(tài)時,那么這個客戶端發(fā)送的命令就會立即被執(zhí)行萎河,否則就會有不同的執(zhí)行路徑:

  • 如果客戶端發(fā)送的命令是EXEC DISCARD WATCH MULTI四個命令中的一個荔泳,那么服務(wù)器立即執(zhí)行這個命令;
  • 否則虐杯,就會將客戶端的命令放到一個事務(wù)隊列里面玛歌,然后給客戶端返回QUEUED回復(fù);
  • 事務(wù)執(zhí)行

當(dāng)一個處于事務(wù)狀態(tài)的客戶端向服務(wù)器發(fā)送EXEC命令時擎椰,這個命令將被立即執(zhí)行契讲,服務(wù)器會遍歷這個客戶端的事務(wù)隊列,執(zhí)行隊列里面保存的所有命令绢慢,最后將執(zhí)行命令的結(jié)果全部返回給客戶端瞪醋。

19.2 WATCH命令的實現(xiàn)

WATCH是一個樂觀鎖,它可以在EXEC執(zhí)行前休弃,監(jiān)視任意數(shù)量的數(shù)據(jù)庫鍵吞歼,并在EXEC執(zhí)行時,檢查被監(jiān)視的鍵是否至少有一個已經(jīng)被修改過了塔猾,如果是的話篙骡,服務(wù)器將拒絕執(zhí)行事務(wù),并向客戶端返回事務(wù)執(zhí)行失敗的空回復(fù)丈甸。

19.3 Redis事務(wù)的ACID性質(zhì)

19.3.1 A(原子性)

原子性指的是數(shù)據(jù)庫事務(wù)將多個操作當(dāng)做一個整體執(zhí)行糯俗,要么都執(zhí)行,要么一個也不執(zhí)行睦擂,對于Redis來說得湘,要么全部執(zhí)行Redis事務(wù)隊列中的所有命令,要么一個也不執(zhí)行顿仇,所以具備原子性淘正;

需要注意的是摆马,Redis的事務(wù)與傳統(tǒng)關(guān)系型數(shù)據(jù)庫事務(wù)的最大區(qū)別就是Redis不支持事務(wù)回滾,即使事務(wù)隊列中的某個命令在執(zhí)行時出錯鸿吆,事務(wù)也不會停止囤采,會繼續(xù)執(zhí)行下去,直到將事務(wù)中的命令全部執(zhí)行完成惩淳;

19.3.1 C(一致性)

一致性是指蕉毯,如果數(shù)據(jù)庫在執(zhí)行事務(wù)之前是一致的,那么事務(wù)執(zhí)行之后也應(yīng)該是一致的思犁,無論事務(wù)是否執(zhí)行成功代虾。一致是指數(shù)據(jù)符合數(shù)據(jù)庫本身的定義,沒有包含非法或者無效的錯誤數(shù)據(jù):

  • 入隊錯誤:如果命令在入隊的時候出現(xiàn)了比如命令不存在等錯誤激蹲,那么Redis將拒絕執(zhí)行這個事務(wù)棉磨;
  • 執(zhí)行錯誤,執(zhí)行錯誤一般是對數(shù)據(jù)庫鍵執(zhí)行了錯誤的類型操作導(dǎo)致托呕,這種錯誤會被Redis識別并進(jìn)行錯誤處理含蓉,這些命令不會對數(shù)據(jù)庫進(jìn)行任何修改,所以不會造成影響
  • 服務(wù)器停機(jī):服務(wù)器停機(jī)之后项郊,如果開啟了Redis持久化馅扣,那么服務(wù)器重啟之后會將數(shù)據(jù)庫狀態(tài)還原到一個一致的狀態(tài),如果沒有開啟持久化着降,那么Redis重啟之后數(shù)據(jù)庫是空白的差油,是一致的狀態(tài);

19.3.1 I (隔離性)

事務(wù)的隔離性是指任洞,即使數(shù)據(jù)庫中多個事務(wù)并發(fā)的執(zhí)行蓄喇,各個事務(wù)之間也不會互相影響,并且在并發(fā)狀態(tài)下執(zhí)行的事務(wù)和串行執(zhí)行的事務(wù)產(chǎn)生的結(jié)果一致交掏;

對于Redis來說妆偏,它使用單線程的方式執(zhí)行事務(wù)(以及事務(wù)隊列中的命令),并且服務(wù)器保證盅弛,在執(zhí)行事務(wù)期間钱骂,不會對事務(wù)進(jìn)行中斷,因此挪鹏,Redis事務(wù)總是以串行的方式執(zhí)行的见秽;

19.3.1 D(耐久性)

事務(wù)的耐久性是指,當(dāng)一個事物執(zhí)行完畢時讨盒,執(zhí)行這個事務(wù)所得的結(jié)果已被保存到永久存儲介質(zhì)里面了解取,即使服務(wù)器停機(jī),執(zhí)行事務(wù)的結(jié)果也不會丟失返顺。

https://cloud.tencent.com/developer/article/1189074
https://juejin.im/post/5cc165816fb9a03202221dd5
https://mp.weixin.qq.com/s?__biz=MzU5ODUwNzY1Nw==&mid=2247484155&idx=1&sn=0c73f45f2f641ba0bf4399f57170ac9b&scene=21#wechat_redirect

Redis分布式鎖

  • (1) setnx (set if not exist)
public boolean tryLock_with_lua(String key, String UniqueId, int seconds) {
    String lua_scripts = "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then" +
            "redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end";
    List<String> keys = new ArrayList<>();
    List<String> values = new ArrayList<>();
    keys.add(key);
    values.add(UniqueId);
    values.add(String.valueOf(seconds));
    Object result = jedis.eval(lua_scripts, keys, values);
    //判斷是否成功
    return result.equals(1L);
}

*(2) SET key value[EX seconds][PX milliseconds][NX|XX]

  • EX seconds: 設(shè)定過期時間禀苦,單位為秒
  • PX milliseconds: 設(shè)定過期時間蔓肯,單位為毫秒
  • NX: 僅當(dāng)key不存在時設(shè)置值
  • XX: 僅當(dāng)key存在時設(shè)置值
  • (3) RedLock

在Redis的分布式環(huán)境中,我們假設(shè)有N個Redis master伦忠。這些節(jié)點完全互相獨立省核,不存在主從復(fù)制或者其他集群協(xié)調(diào)機(jī)制稿辙。我們確保將在N個實例上使用與在Redis單實例下相同方法獲取和釋放鎖±ヂ耄現(xiàn)在我們假設(shè)有5個Redis節(jié)點,同時我們需要在5臺服務(wù)器上面運行這些Redis實例邻储,這樣保證他們不會同時都宕掉赋咽。

為了取到鎖,客戶端應(yīng)該執(zhí)行以下操作:

  • 獲取當(dāng)前Unix時間吨娜,以毫秒為單位脓匿。
  • 依次嘗試從5個實例,使用相同的key和具有唯一性的value(例如UUID)獲取鎖宦赠。當(dāng)向Redis請求獲取鎖時陪毡,客戶端應(yīng)該設(shè)置一個網(wǎng)絡(luò)連接和響應(yīng)超時時間,這個超時時間應(yīng)該小于鎖的失效時間勾扭。例如你的鎖自動失效時間為10秒毡琉,則超時時間應(yīng)該在5-50毫秒之間。這樣可以避免服務(wù)器端Redis已經(jīng)掛掉的情況下妙色,客戶端還在死死地等待響應(yīng)結(jié)果桅滋。如果服務(wù)器端沒有在規(guī)定時間內(nèi)響應(yīng),客戶端應(yīng)該盡快嘗試去另外一個Redis實例請求獲取鎖身辨。
  • 客戶端使用當(dāng)前時間減去開始獲取鎖時間(步驟1記錄的時間)就得到獲取鎖使用的時間丐谋。當(dāng)且僅當(dāng)從大多數(shù)(N/2+1,這里是3個節(jié)點)的Redis節(jié)點都取到鎖煌珊,并且使用的時間小于鎖失效時間時号俐,鎖才算獲取成功。
  • 如果取到了鎖定庵,key的真正有效時間等于有效時間減去獲取鎖所使用的時間(步驟3計算的結(jié)果)吏饿。
  • 如果因為某些原因,獲取鎖失斚捶 (沒有在至少N/2+1個Redis實例取到鎖或者取鎖時間已經(jīng)超過了有效時間)找岖,客戶端應(yīng)該在所有的Redis實例上進(jìn)行解鎖(即便某些Redis實例根本就沒有加鎖成功,防止某些節(jié)點獲取到鎖但是客戶端沒有得到響應(yīng)而導(dǎo)致接下來的一段時間不能被重新獲取鎖)敛滋。

無論如何许布,在解鎖的時候需要check設(shè)置的value,這應(yīng)該是一個唯一的值绎晃,不是每個客戶端都可以解鎖蜜唾,只有設(shè)置這個可以的客戶端才能解鎖杂曲。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市袁余,隨后出現(xiàn)的幾起案子擎勘,更是在濱河造成了極大的恐慌,老刑警劉巖颖榜,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件棚饵,死亡現(xiàn)場離奇詭異,居然都是意外死亡掩完,警方通過查閱死者的電腦和手機(jī)噪漾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來且蓬,“玉大人欣硼,你說我怎么就攤上這事《褚酰” “怎么了诈胜?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長冯事。 經(jīng)常有香客問我焦匈,道長,這世上最難降的妖魔是什么桅咆? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任括授,我火速辦了婚禮,結(jié)果婚禮上岩饼,老公的妹妹穿的比我還像新娘荚虚。我一直安慰自己,他們只是感情好籍茧,可當(dāng)我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布版述。 她就那樣靜靜地躺著,像睡著了一般寞冯。 火紅的嫁衣襯著肌膚如雪渴析。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天吮龄,我揣著相機(jī)與錄音俭茧,去河邊找鬼。 笑死漓帚,一個胖子當(dāng)著我的面吹牛母债,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼毡们,長吁一口氣:“原來是場噩夢啊……” “哼迅皇!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起衙熔,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤登颓,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后红氯,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體框咙,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年脖隶,在試婚紗的時候發(fā)現(xiàn)自己被綠了扁耐。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡产阱,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出块仆,到底是詐尸還是另有隱情构蹬,我是刑警寧澤,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布悔据,位于F島的核電站庄敛,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏科汗。R本人自食惡果不足惜藻烤,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望头滔。 院中可真熱鬧怖亭,春花似錦、人聲如沸坤检。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽早歇。三九已至倾芝,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間箭跳,已是汗流浹背晨另。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留谱姓,地道東北人借尿。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像逝段,于是被迫代替她去往敵國和親垛玻。 傳聞我的和親對象是個殘疾皇子割捅,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,700評論 2 354