第二章 簡單動態(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
下面是一個跳躍表圖片:
查找的時候先從最高層開始找,逐漸二分下降層數(shù)胞锰,整體上就是一個二分查找算法的最佳實踐遗淳。
下面是一個再跳躍表里面查找23這個值的例子:
第六章 整數(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è)置這個可以的客戶端才能解鎖杂曲。