第二章 簡單動態(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'恶迈,表示字符串結束涩金。
2、SDS和C字符串的區(qū)別
2.1 常數(shù)復雜度獲取字符串長度
對于C字符串來說暇仲,因為沒有記錄字符串長度步做,所以如果想要知道字符串長度,則需要遍歷一次字符串奈附,這個操作的時間復雜度為O(N)全度,而SDS中的len屬性記錄了SDS當前字符串的長度,所以可以直接獲取斥滤,這個操作的時間復雜度是O(1)将鸵;而且SDS的len屬性是在操作SDS API的時候自動完成的,不需要自己維護佑颇,所以不需要擔心安全問題顶掉;
2.2 SDS杜絕緩沖區(qū)溢出
因為C字符串不記錄自身長度的原因,所以容易造成緩沖區(qū)溢出挑胸,比如痒筒,有兩個字符串在內存中緊挨著:
... 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剩余的內存空間無法再容納src,則就會發(fā)生緩沖區(qū)溢出解藻,比如執(zhí)行:
strcat(S1, "CLUSTER")老充,內存中的S2字符串就會被覆蓋掉:
... R E D I S C L U S T E R \0 ...
而SDS在進行字符串修改之前,會先檢查SDS的空間是否滿足修改所需的空間要求螟左,如果不滿足啡浊,則會先擴展SDS的空間,然后再進行修改胶背。
2.3 SDS減少修改字符串時帶來的內存重分配次數(shù)
對于C字符串來說虫啥,將字符串變長或者縮短之前,都需要進行內存重新分配奄妨,否則會出現(xiàn)問題涂籽,如果字符串變長之前不進行內存重新分配,則會出現(xiàn)緩沖區(qū)溢出問題砸抛,如果在縮短字符串之前不進行內存重分配评雌,則會發(fā)生內存泄漏問題树枫;
SDS使用free屬性來代表buf中還有多少字符可以使用,SDS使用空間預分配和惰性空間釋放策略來避免這個問題景东;
2.3.1 空間預分配
SDS在需要對空間進行擴展的時候砂轻,會額外擴展一些空間,使用free屬性來維護斤吐,避免頻繁分配內存搔涝;額外分配的內存大小由以下公式?jīng)Q定:
- 如果對SDS進行修改之后,SDS的長度(len屬性)將小于1MB和措,那么程序將分配和len屬性同樣大小的未使用空間庄呈,比如,在SDS進行修改之后派阱,len屬性變?yōu)?3字節(jié)诬留,那么程序也會分配13字節(jié)未使用的空間(free屬性),此時free和len相等贫母,buf的實際長度為13 + 1 + 13 = 27字節(jié)文兑;
- 如果對SDS進行修改之后,SDS的長度大于1MB空間腺劣,那么SDS會分配1MB的未使用空間绿贞;
2.3.2 惰性空間釋放
在SDS字符串進行縮短字符串操作時,SDS并不會執(zhí)行內存重分配將對于的空間進行回收橘原,而是記錄在free屬性里面籍铁,等待后續(xù)字符串擴大時使用,這就避免了字符串造成時的頻繁內存操作靠柑;當然寨辩,如果確認字符串空間已經(jīng)足夠吓懈,那么可以調用SDS的API進行內存回收歼冰。
2.4 二進制安全
C語言中使用空字符'\0'來表示字符串的末尾,也就是字符串中間不能存在空字符串耻警,否則字符串就會被截斷隔嫡,而對于一些二進制數(shù)據(jù),難免會使用到空字符甘穿,而C字符串就無法表示這些數(shù)據(jù)腮恩,SDS使用buf來存儲字符串,存儲的是什么温兼,讀出來的就是什么秸滴,是二進制安全的表示方式。所以Redis不僅可以保存字符串募判,還可以保存二進制文件荡含,比如視頻咒唆、圖片等。
2.5 兼容部分C字符串函數(shù)
SDS的buf里面存儲的字符串依然保持以'\0'結尾的慣例释液,所以可以使用部分C字符串函數(shù)全释,比如strcat,這樣误债,SDS就不需要再重復編寫一遍字符串操作函數(shù)了浸船。
第三章 鏈表
3.1 Redis鏈表實現(xiàn)的特性
- 雙端鏈表:每個鏈表節(jié)點帶有prev和next指針,獲取某個節(jié)點的前置或者后置節(jié)點的時間復雜度為O(1)寝蹈;
- 無環(huán):表頭的prev和表尾的next都指向NULL李命,對鏈表的訪問以NULL為結尾;
- 帶表頭指針和表位指針:通過list的head和tail指針躺盛,獲取表頭或者表尾的時間復雜度為O(1)项戴;
- 帶鏈表長度計數(shù)器:使用list結構的len屬性來表示鏈表的長度,程序獲取鏈表長度的時間復雜度為O(1)
- 多態(tài):鏈表的每個節(jié)點使用void*指針來保存節(jié)點的值槽惫,并且通過dup周叮、free、match三個屬性為節(jié)點設置類型特定函數(shù)界斜,所以鏈表合一用于保存各種不同類型的值仿耽;
第四章 字典
字典:又被稱為符號表、關聯(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 索引,當rehash不在進行時疗认,值為-1
int rehashidx;
} dict;
ht屬性是包含兩個哈希表的數(shù)組完残,一般情況下,只有ht[0]會被使用横漏,ht[1]只有在rehash的時候才會用到谨设,rehashidx用于標記rehash的進度,如果當前不在進行rehash操作缎浇,則rehashidx為-1扎拣;
4.4 哈希算法
當要將一個新的鍵值對添加到字典中時,需要先根據(jù)鍵值對的鍵來計算出哈希值和索引值,然后根據(jù)索引值將包含新鍵值對的哈希節(jié)點放到對應的位置上去二蓝;
計算哈希值的函數(shù)在字典的type屬性里面尊蚁;
當一個數(shù)組的長度是2的冪次方時,有一個很好的特性是侣夷,
index = hash & (len -1)
這可能就是sizemask的作用横朋;
4.5 解決鍵沖突
當有兩個以上的鍵被分配到同一個索引上的時候,稱這些鍵發(fā)生了沖突百拓,Redis使用鏈地址法來解決哈希沖突琴锭,每個鍵值對節(jié)點都是一個鏈表節(jié)點,包含next指針衙传;
4.6 rehash
隨著操作的不斷執(zhí)行决帖,哈希表保存的鍵值對會逐漸的增多或者減少,為了讓哈希表的負載因子保持在一個合理的范圍蓖捶,當哈希表的鍵值對太多或者太少時地回,程序需要進行rehash操作。
Redis進行rehash操作的步驟如下:
- 為字典ht[1]分配空間俊鱼,分配的大小取決于要執(zhí)行的操作刻像,和ht[0]當前包含的鍵值對數(shù)量(ht[0].used)
- 如果執(zhí)行的擴展操作,那么ht[1]的大小為第一個大于等于 ht[0].used * 2 的2次冪并闲;
- 如果執(zhí)行的是收縮操作细睡,那么ht[1]的大小為第一個大于等于ht[0].used的2次冪;
- 將保存在ht[0]中的所有鍵值對rehash到ht[1]上面
- 當ht[0]的所有鍵值對都rehash到ht[1]之后帝火,釋放ht[0]溜徙,然后將ht[1]當做ht[0];
4.7 哈希表的收縮與擴展
當以下條件任意一個滿足時犀填,程序會自動開始對哈希表進行擴展操作:
- 服務器目前沒有執(zhí)行BGSAVE或者BGREWRITEAOF命令蠢壹,并且哈希表的負載因子大于等于1;
- 服務器正在執(zhí)行BGSAVE或者BGREWRITEAOF命令九巡,并且哈希表的負載因子大于等于5图贸;
負載因子的計算公式為:
load_factor = ht[0].used / ht[0].size
Redis在執(zhí)行BGSAVE或者BGREWRITEAOF命令時,需要fork出紫子進程比庄,大多數(shù)操作系統(tǒng)采用寫時復制的技術來優(yōu)化進程的內存使用效率求妹,如果在這個時候進行rehash乏盐,子進程也需要寫佳窑,如果避免在這個時候進行擴展,則可以最大限度的節(jié)約內存父能。
當哈希表的負載因子小于0.1時神凑,程序會自動開始對哈希表進行收縮操作。
4.8 漸進式rehash
rehash操作并不是一次性完成的,因為哈希表中的鍵值對數(shù)量可能非常大溉委,如果一次性完成所有鍵值對的rehash操作鹃唯,那么Redis可能會停止服務而進行rehash,為了避免這個問題瓣喊,Redis采用分多次坡慌,漸進式的完成rehash操作;
下面是Redis進行漸進式rehash的詳細步驟:
- 為ht[1]分配空間藻三,讓字典同時持有ht[0]和ht[1]兩個哈希表
- 在字典中維護一個索引計數(shù)器變量rehashidx洪橘,并將它的值設置為0,表示rehash工作正式開始棵帽;
- 在rehash進行期間熄求,每次對字典的訪問操作,程序除了執(zhí)行指定的操作之外逗概,還會順帶將ht[0]哈希表在rehashidx索引上的鍵值對rehash到ht[1]弟晚,當rehash完成之后,rehashidx + 1逾苫,表示下一次rehash操作的鍵值對索引是rehashidx + 1
- 隨著字典的不斷操作卿城,到某個時間點,ht[0]上的所有鍵值對都會rehash到ht[1]铅搓,此時設置rehashidx = -1藻雪,表示rehash已經(jīng)完成;
在rehash期間狸吞,字典會同時持有ht[0]和ht[1]兩張哈希表勉耀,對于查找來說,會現(xiàn)在ht[0]找蹋偏,找不到再去ht[1]找便斥, 對于刪除操作也是需要操作兩張哈希表,而對于插入操作威始,所有新插入的鍵值對只會在ht[1]里面枢纠,所以保證了ht[0]里面的鍵值對只減不增,這樣rehash總是會結束黎棠。
第五章 跳躍表
跳躍表是有序列表的底層實現(xiàn)數(shù)據(jù)結構晋渺,Redis的跳躍表實現(xiàn)包括zskiplist和zskiplistNode兩個結構,每個跳躍表的層高都是1~32之間的隨機數(shù)脓斩,在同一個跳躍表中木西,每一個節(jié)點的成員變量必須唯一(類似于Set),當多個節(jié)點的分值相同時随静,節(jié)點按照成員對象的大小進行排序八千。
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ù)組以有序沸停,無重復的方式存儲集合元素膜毁,當有需要的時候會改變數(shù)組的類型(encode)
- 升級操作為整數(shù)集合帶來了操作上的靈活性,并且盡可能的節(jié)約了內存愤钾;
- 整數(shù)集合只支持升級操作爽茴,不支持降級操作
第七章 壓縮列表
壓縮列表是列表鍵和哈希鍵的底層實現(xiàn)之一,當一個列表鍵只包含少量的列表項绰垂,并且每個列表項要么就是小整數(shù)值室奏,要么就是簡短的字符串,那么Redis就會使用壓縮列表來進行實現(xiàn)劲装;
當一個哈希鍵只包含少量鍵值對胧沫,并且每個鍵值對的鍵和值要么是小整數(shù),要么是簡短的字符串占业,那么Redis就會使用壓縮列表來實現(xiàn)哈希鍵绒怨。
壓縮列表是Redis為了節(jié)約內存而開發(fā)的一種順序存儲數(shù)據(jù)結構。
第八章 對象
Redis基于前面介紹的那些數(shù)據(jù)結構谦疾,構建了一個對象系統(tǒng)南蹂,這個系統(tǒng)包括字符串對象、列表對象念恍、哈希對象六剥、集合對象和有序集合對象五種類型的對象。
Redis通過引用計數(shù)技術峰伙,當程序不再使用某個對象時疗疟,這個對象所占用的內存就會被自動釋放,還基于引用計數(shù)實現(xiàn)了對象共享機制瞳氓,從而來實現(xiàn)節(jié)約內存的目的策彤。
Redis的對象還記錄了訪問時間,這個信息可以用于計算數(shù)據(jù)庫鍵的空轉時間匣摘,空轉時間較長的那些對象有可能被服務器刪除掉店诗。
8.1 對象類型和編碼
Redis使用對象來表示數(shù)據(jù)庫中的鍵值對,每當我們在數(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ù)結構的指針
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ù)結構』Т牵可以通過OBJECT ENCODING 命令來查看編碼信息泌类;Redis沒有將一種類型關聯(lián)到一種特定類型的編碼上,極大的提升了Redis的靈活性和效率底燎,Redis可以根據(jù)對象的特征進行不同的編碼刃榨,比如,在一個列表鍵包含較少的列表項時双仍,Redis就會使用壓縮列表來實現(xiàn)枢希,而當壓縮列表不再適應的情況下,就會使用列表來實現(xiàn)朱沃。
8.2 字符串對象
字符串對象的編碼可以是int苞轿、raw或者embstr。
- 如果一個字符串保存的是整數(shù)值逗物,那么編碼就是int搬卒;
- 如果字符串對象保存的是一個字符串,并且字符串長度大于32字節(jié)翎卓,那么就會使用SDS來表示契邀,編碼為raw;
- 如果字符串對象保存的是一個長度小于等于32字節(jié)的字符串失暴,那么就會使用embstr編碼來保存坯门;
- 如果保存的是浮點數(shù),那么也是使用字符串的方式保存逗扒;
embstr編碼和raw的效果是一樣的田盈,但是embstr使用一次內存分配/回收來管理RedisObject結構和sdshdr結構吴侦,而raw會調用兩次內存分配侍芝。并且embstr的redisObject和sdshdr是連續(xù)分布的弱匪,所以可以更好的利用緩存帶來的優(yōu)勢谊惭。
8.3 列表對象
列表對象的編碼可以是ziplist或者linkedlist孵奶。當列表對象可以同時滿足下面兩個條件時眷蜈,使用ziplist來保存:
- 列表對象保存的所有字符串長度都小于64字節(jié)预厌;
- 列表對象保存的元素數(shù)量少于512個叨橱;
如果不能滿足這兩個對象建炫,則使用linkedlist來實現(xiàn)畦韭;對于使用ziplist的列表對象,如果其中一個條件不再滿足的時候肛跌,就會進行編碼轉換艺配,轉換成使用linkedlist來進行保存察郁。
8.4 哈希對象
哈希對象的編碼可以是ziplist或者hashtable,當哈希對象可以同時滿足下面兩個條件時转唉,使用ziplist皮钠,否則使用hashtab:
- 哈希對象保存的所有鍵值對的鍵和值的字符串長度都小于64字節(jié);
- 哈希對象保存的鍵值對數(shù)量小于512個赠法;
8.5 集合對象
集合對象的編碼可以是intset麦轰、hashtable,當集合對象可以同時滿足下面兩個條件時砖织,使用intset款侵,否則使用hashtabe:
- 集合對象保存的所有元素都是整數(shù)值;
- 集合對象保存的元素數(shù)量不超過512個侧纯;
8.6 有序集合對象
有序集合對象的編碼可以是ziplist或者skiplist新锈,在Redis中,同時使用了跳躍表和字典來實現(xiàn)有序集合眶熬,zset的zsl屬性是一個跳躍表壕鹉,它按照元素的分值從小打到的保存元素,這樣就可以實現(xiàn)比如范圍操作之類的功能聋涨,而zset的dict屬性使用字段來存儲了集合的成員到分值的映射晾浴,這樣基于成員查找分值的時間復雜度就是O(1),同時需要說明的是牍白,Redis使用對象共享技術脊凰,所以同時使用跳躍表和字典不會造成內存浪費。
同時使用跳躍表和字典來實現(xiàn)有序集合茂腥,主要是為了實現(xiàn)效率上的考慮狸涌,如果只使用字典來實現(xiàn),那么雖然根據(jù)成員查詢分值的時間復雜度是O(1)最岗,但是因為字典以無序的方式存儲元素帕胆,所以在執(zhí)行范圍操作的時候,就需要先排序再操作般渡,時間復雜度是O(NlogN)懒豹,還要創(chuàng)建一個額外的數(shù)組來存儲排序后的元素(O(N)),如果只使用跳躍表驯用,那么雖然范圍查找沒問題脸秽,但是基于成員查詢分值的操作就會從O(1)提升到O(N),所以同時使用兩種數(shù)據(jù)結構來實現(xiàn)蝴乔。
當有序集合對象可以同時滿足下面兩個條件時记餐,對象使用ziplist來實現(xiàn),否則使用skiplist來實現(xiàn):
- 有序集合保持的元素數(shù)量小于128個薇正;
- 有序集合保存的所有元素的長度小于64字節(jié)片酝;
8.8 內存回收
Redis使用引用計數(shù)來實現(xiàn)對象跟蹤囚衔,對象的引用計數(shù)會隨著對象的使用而發(fā)生變化:
- 在創(chuàng)建一個新對象時,引用計數(shù)的值會被初始化為1雕沿;
- 當一個對象被一個程序引用時练湿,它的引用計數(shù)值會加1;
- 當對象不再被一個程序使用時晦炊,它的引用計數(shù)會減1鞠鲜;
- 當對象的引用計數(shù)變?yōu)?時宁脊,對象所占用的內存會被釋放断国;
8.9 對象共享
當A和B需要的對象一樣時,Redis就會使用對象共享技術榆苞,不會創(chuàng)建多個對象稳衬,而是會復用現(xiàn)有的對象。Redis只會共享包含整數(shù)值的字符串對象坐漏,因為其他的字符串對象的對比需要消耗更多的CPU薄疚,得不償失;
8.10 對象的空轉時常
每個對象都會記錄對象最后一次被訪問的時間戳赊琳,當執(zhí)行命令OBJECT IDLETIME時街夭,就是將當前時間減去對象的這個時間戳,需要注意的是躏筏,這個命令執(zhí)行時板丽,對象的lru(最后訪問時間戳)不會被更新〕媚幔空轉時間還可以用于Redis服務器淘汰鍵值對埃碱,如果開啟了maxmemory選項,并且服務器用于回收內存的算法為valatile-lru或者allkeys-lru時酥泞,那么服務器占用的內存數(shù)超過maxmemory時砚殿,就會將空轉時間較長的部分鍵值對優(yōu)先釋放內存;
第九章 數(shù)據(jù)庫
Redis默認情況下會初始化16個數(shù)據(jù)庫芝囤,每個數(shù)據(jù)庫由redisDb結構表示似炎。
每個Redis客戶端都會有自己的目標數(shù)據(jù)庫,在客戶端執(zhí)行讀寫數(shù)據(jù)庫的時候悯姊,目標數(shù)據(jù)庫就會成員操作對象名党。默認情況下,目標數(shù)據(jù)庫為0號數(shù)據(jù)庫挠轴,可以通過執(zhí)行SELECT命令來切換數(shù)據(jù)庫传睹;在服務器內部的redisClient結構的db屬性記錄了客戶端當前的目標數(shù)據(jù)庫,這個屬性是指向redisDb結構的指針岸晦;
Redis的redisDb結構里面有一個dict屬性欧啤,它記錄了數(shù)據(jù)庫中的所有鍵值對睛藻,我們將這個字段稱為鍵空間。
Redis的redisDb結構中的expires字典保存了數(shù)據(jù)庫中所有鍵的過期時間邢隧,我們稱這個字典為過期字典店印,當執(zhí)行了為鍵值對設置過期時間的命令之后,就會在expires字典里面插入一個新的鍵值對倒慧,值為過期時間按摘。當為某個鍵移除過期時間時,相應的也會在過期字典里面將其刪掉纫谅。
9.1 過期鍵刪除策略
如果一個鍵過期了炫贤,那么它被刪除的策略有三種:
- 定時刪除:在設置鍵的過期時間的同時,設置一個定時器付秕,定時器在鍵的過期時間來臨時兰珍,立即刪除鍵;
- 惰性刪除:放任鍵過期不管询吴,但是每次從鍵空間中獲取鍵時掠河,都檢查鍵是否已經(jīng)過期,如果過期的話猛计,則刪除鍵唠摹,否則返回;
- 定期刪除:每隔一段時間奉瘤,就對數(shù)據(jù)庫進行一次檢查勾拉,刪除里面的過期鍵,至于要刪除多少鍵毛好,以及檢查多少個數(shù)據(jù)庫望艺,則由算法決定;
定時刪除對內存最優(yōu)化肌访,那些過期的鍵都能及時被清理找默,而如果有大量的鍵需要清理則對CPU不太友好,需要大量的CPU時間吼驶;惰性刪除對內存不太友好惩激,但是對CPU友好;定期刪除策略是對前兩種策略的折中方案:
- 定期刪除策略每隔一段時間執(zhí)行一次過期鍵刪除操作蟹演,并通過限制刪除操作執(zhí)行的時常和頻率來減少刪除操作對CPU時間的影響风钻;
- 除此之外,通過定期刪除過期鍵酒请,可以減少過期鍵帶來的內存浪費骡技;
Redis實際使用的是惰性刪除和定期刪除兩種策略,服務器可以很好的在合理使用CPU和避免內存空間浪費之間取得平衡;
9.1.1 惰性刪除策略的實現(xiàn)
過期鍵的惰性刪除由db.c/expireIfNeeded函數(shù)實現(xiàn)布朦,所有讀寫Redis數(shù)據(jù)庫的命令在執(zhí)行之前都會調用這個函數(shù)進行鍵過期檢查囤萤;
- 如果輸入鍵已經(jīng)過期,那么函數(shù)將輸入鍵從數(shù)據(jù)庫中刪除是趴;
- 如果輸入鍵不過期涛舍,那么這個函數(shù)什么也不做;
9.1.2 定期刪除策略的實現(xiàn)
定期刪除由redis.c/activeExpieCycle函數(shù)實現(xiàn)唆途,每當Redis的服務器周期性的執(zhí)行serverCorn函數(shù)時富雅,activeExpieCycle函數(shù)也會被調用,它在規(guī)定的時間內肛搬,分多次遍歷服務器中的各個數(shù)據(jù)庫没佑,從數(shù)據(jù)庫的expires字典中隨機檢查一部分鍵的過期時間,并刪除其中的過期鍵滚婉。
9.2 AOF图筹、RDB和復制功能對過期鍵的處理
9.2.1 生成RDB文件
在執(zhí)行SAVE或者BGSACE命令時帅刀,程序會對數(shù)據(jù)庫中的鍵進行檢查让腹,已經(jīng)過期的鍵不會被保存到數(shù)據(jù)庫中;
9.2.2 載入RDB文件
在啟動Redis服務器時扣溺,如果服務器開啟了RDB功能骇窍,那么服務器將對RDB文件進行載入:
- 如果服務器以主服務器模式運行,那么在載入RDB文件時锥余,程序會對保存的鍵進行檢查腹纳,未過期的鍵會被保存到數(shù)據(jù)庫中,而過期的鍵則會被忽略驱犹。
- 如果服務器以從服務器運行嘲恍,那么在載入RDB文件時,文件中保存的所有鍵都會被載入到數(shù)據(jù)庫中雄驹,不過佃牛,因為主服務器在進行數(shù)據(jù)同步時,從服務器的數(shù)據(jù)庫就會被清空医舆,所以一般來講對從服務器也沒有影響俘侠。
9.2.3 AOF文件寫入
如果鍵過期被惰性刪除或者定期刪除了,那么會向AOF文件寫入一條DEL命令蔬将,來顯示的記錄該鍵已經(jīng)被刪除爷速;
9.2.4 復制
當服務器運行在復制模式下時,從服務器的過期鍵刪除動作由主服務器控制:
- 主服務器在刪除一個過期鍵之后霞怀,會顯示的向所有從服務器發(fā)送一條DEL命令惫东,告訴從服務器刪除這個過期鍵;
- 從服務器沒有權利刪除過期鍵毙石,所以從服務器在遇到過期鍵時不會刪除過期鍵廉沮,它會等待主服務器來控制自己禁谦。
第十章 RDB持久化
RDB文件會將當前Redis數(shù)據(jù)庫中的所有鍵值對記錄到一個二進制文件中,有兩個命令可以生成RDB文件废封,SAVE和BGSAVE州泊;
SAVE命令會阻塞服務器進程,直到RDB文件生成完成漂洋,BGSAVE則會派生出一個子進程來負責生成RDB文件遥皂,而主進程依然可以處理Redis的其他命令。RDB文件的載入沒有命令刽漂,是Redis在啟動的時候自動完成的演训,只要Redis檢測到RDB文件,就會自動載入RDB文件到內存贝咙,需要注意的是样悟,因為AOF文件的更新頻率更高,所以:
- 如果服務器開啟了AOF持久化功能庭猩,那么服務器會優(yōu)先使用AOF文件來還原數(shù)據(jù)庫窟她;
- 只有在AOF功能處于關閉狀態(tài),服務器才會使用RDB文件來還原數(shù)據(jù)庫蔼水;
第十一章 AOF持久化
AOF和RDB不一樣震糖,AOF保存的是執(zhí)行過的命令,Redis會將所有執(zhí)行過寫命令追加到AOF文件中去趴腋,啟動Redis服務器的時候恢復這個數(shù)據(jù)庫吊说;
AOF分為命令追加、文件寫入优炬、文件同步三個部分颁井;
11.1 命令追加
當AOF持久化功能處于打開狀態(tài)時,服務器在執(zhí)行完一個命令之后蠢护,會按照協(xié)議格式將被執(zhí)行的命令追加到服務器狀態(tài)的aof_buf緩沖區(qū)的末尾雅宾;
11.2 文件寫入 與同步
Redis的服務器其實就是一個事件循環(huán),包括文件循環(huán)和時間循環(huán)糊余,服務器在處理文件事件時可能會往aof_buf里面寫入內容秀又,所以在每次結束一次循環(huán)之前,會調用flushAppendOnlyFile函數(shù)贬芥,考慮是否需要將aof_buf里面的內容保存到AOF文件里面去吐辙。flushAppendOnlyFile函數(shù)的行為由appendfasync選項的值來決定:
- always:將aof_buf里面的內容寫入并同步到AOF文件;
- everysec:將aof_buf里面的內容寫入到AOF文件蘸劈,如果上次同步aof文件的時間距離現(xiàn)在超過1秒昏苏,那么再次對AOF文件進行同步,并且這個事情由一個專門的線程負責;
- no:將aof_buf里面的內存寫入到AOF文件贤惯,但并不對AOF文件進行同步洼专,何時同步由操作系統(tǒng)決定;
默認為everysec孵构。
11.3 AOF重寫
因為AOF持久化是通過保存redis執(zhí)行的命令來實現(xiàn)的屁商,隨著服務器運行,這個文件會越來越大颈墅,甚至會影響redis服務器蜡镶,所以需要重寫AOF文件,重寫是通過讀取redis當前數(shù)據(jù)庫狀態(tài)來實現(xiàn)的恤筛;比如原來一個列表是通過10次添加元素構成的官还,重寫之后只需要一條命令即可,大大縮短了命令的數(shù)量毒坛。
AOF重寫需要執(zhí)行大量的寫入操作望伦,所以這個函數(shù)的線程將長時間阻塞,這就會影響redis服務器處理正常的請求煎殷,所以AOF重寫的任務被設計成一個后臺進程來完成屯伞;
不過,這里需要注意的是蝌数,子進程在進行AOF重寫的時候愕掏,主進程還在繼續(xù)處理請求度秘,而新的命令可能會對現(xiàn)有的數(shù)據(jù)庫狀態(tài)進行變更顶伞,從而使得當前的數(shù)據(jù)庫狀態(tài)和重寫后的AOF文件所保存的數(shù)據(jù)庫狀態(tài)不一致,為了解決這個問題剑梳,Redis服務器設置了一個AOF重寫緩沖區(qū)唆貌,這個緩存區(qū)在服務器創(chuàng)建子進程之后開始使用,當Redis服務器執(zhí)行完一個寫命令之后垢乙,它會同時將這個寫命令發(fā)送給AOF緩沖區(qū)和AOF重寫緩沖區(qū)锨咙,當子進程完成AOF重寫工作之后,它會向父進程發(fā)送一個信號追逮,父進程在接收到這個信號之后酪刀,會進程信號處理:
- 將AOF重寫緩沖區(qū)中的內容全部寫入到新的AOF文件中;
- 對新的AOF文件進行改名钮孵,原子的覆蓋原來的AOF文件骂倘;
第十二章 事件
Redis服務器是一個事件驅動的程序,服務器需要處理兩類事件:
- 文件事件:也就是I/O事件巴席,比如客戶端的連接上的讀寫請求历涝;
- 時間事件:某些操作(比如serverCorn)需要在給定的時間點運行,這就是時間事件;
12.1 文件事件
Redis基于Reactor模式開發(fā)了自己的事件處理器荧库,這個處理器被稱為文件事件處理器(file event handler)
- 文件事件處理器使用I/O多路復用技術堰塌,同時監(jiān)聽多個套接字,并根據(jù)套接字目前執(zhí)行的任務設置不同的事件處理器分衫;
- 當被監(jiān)聽的套接字上有讀寫事件發(fā)生時场刑,文件事件處理器就會調用相應的處理器來處理這些事件;
12.2 時間事件
服務器將所有的時間事件都放在一個無序列表里面蚪战,每當時間事件執(zhí)行器運行時摇邦,它就遍歷整個列表,查找所有已到達的時間事件屎勘,并調用相應的時間處理器施籍。一般情況下服務器只會執(zhí)行serverCorn一個時間事件。
第十五章 復制
在Redis中概漱,可以執(zhí)行SLAVEOF命令或者設置slaveof選項丑慎,讓一個服務器復制另外一個服務器,我們稱呼被復制的服務器為主服務器(master)瓤摧,而對主服務器進行復制的則成為從服務器(slave)竿裂。
15.1 舊版本復制功能的實現(xiàn)(2.8版本之前)
Redis的復制分為同步(sync)和命令傳播(command propagate)兩個操作:
- 同步:同步操作用于將從服務器的數(shù)據(jù)庫狀態(tài)更新至服務器所處的數(shù)據(jù)庫狀態(tài);
- 命令傳播:命令傳播用于在主服務器的數(shù)據(jù)庫狀態(tài)被修改照弥,導致主從服務器數(shù)據(jù)庫狀態(tài)不一致時腻异,讓主從服務器的數(shù)據(jù)庫重新回到一致狀態(tài);
15.1.1 同步(sync)
當客戶端向從服務器發(fā)送SLAVEOF命令这揣,要求復制主服務器時悔常,從服務器首先需要執(zhí)行同步操作,首先將從服務器的數(shù)據(jù)庫狀態(tài)更新到主服務器一致给赞。
從服務器通過向主服務器發(fā)送SYNC命令來完成同步操作机打,以下是SYNC命令的執(zhí)行步驟:
- 從服務器向主服務器發(fā)送SYNC命令;
- 收到SYNC命令的主服務器開始執(zhí)行BGSAVE命令片迅,在后臺生成一個RDB文件残邀,并使用一個緩沖區(qū)記錄從現(xiàn)在開始執(zhí)行的所有寫命令;
- 當主服務器的BGSAVE命令執(zhí)行完成時柑蛇,主服務器會將生成的RDB文件發(fā)送給從服務器芥挣,從服務器接收并載入這個RDB文件;
- 主服務器將記錄在緩存區(qū)里面的所有寫命令發(fā)送給從服務器耻台,從服務器執(zhí)行這些寫命令空免,將自己的狀態(tài)更新至主服務器當前的狀態(tài)。
15.1.2 命令傳播(command propagate)
當同步操作完成之后粘我,主從服務器兩者的數(shù)據(jù)庫狀態(tài)將達到一致狀態(tài)鼓蜒,但是這種狀態(tài)并不是一成不變的痹换,如果主服務器的數(shù)據(jù)庫被改變,則一致就不再存在都弹。
為了讓從服務器和主服務器保持一致娇豫,主服務器需要對從服務器執(zhí)行命令傳播,主服務器會將自己執(zhí)行的寫命令畅厢,也就是會造成主從數(shù)據(jù)庫狀態(tài)不一致的命令冯痢,發(fā)送給從服務器執(zhí)行,當從服務器執(zhí)行之后框杜,主從數(shù)據(jù)庫狀態(tài)又會變成一致浦楣。
15.2 舊版(2.8版本之前)復制功能的缺陷
在Redis中,從服務器對主服務器的復制可以分為以下兩種情況:
- 初次復制:從服務器以前沒用復制過任何主服務器咪辱,或者從服務器當前要復制的主服務器和上一次復制的主服務器不同振劳;
- 斷線后重新復制:處于命令傳播階段的主從服務器因為網(wǎng)絡原因中斷了復制,但從服務器通過自動重連連接上了主服務器油狂,并繼續(xù)復制主服務器历恐;
對于初次復制來說,舊版復制功能沒有問題专筷,但是對于斷線重連后的復制弱贼,舊版雖然也可以讓主從服務器重新回到一致狀態(tài),但是效率卻非常低磷蛹。因為斷線重連后從服務器將重新進行同步吮旅,執(zhí)行SYNC命令,這個命令是消除消耗資源的味咳。
15.3 新版復制功能的實現(xiàn)
為了解決舊版復制功能在處理斷線重新復制情況下的低效問題庇勃,從Redis 2.8開始,使用PSYNC命令進行復制中的同步操作莺葫,PSYC命令有完整重同步和部分重同步兩種模式:
- 完整重同步用于初次復制的情況:完整重同步和SYNC命令操作基本是一樣的匪凉;
- 部分重同步用于斷線后重復制的情況:當從服務器斷線后重新連接到主服務器,如果條件允許捺檬,主服務器可以將斷線期間執(zhí)行過的寫命令發(fā)送給從服務器,就可以將從服務器的數(shù)據(jù)庫更新成和主服務器一致贸铜;
15.4 部分重同步實現(xiàn)
部分重同步由三部分組成
- 主服務器的復制偏移量和從服務器的復制偏移量(replication offset)
- 主服務器的復制積壓緩沖區(qū)(replication backlog)
- 服務器的運行ID(run ID)
15.4.1 復制偏移量
執(zhí)行復制的雙方會分別維護一個復制偏移量:
- 主服務器每次向從服務器傳播N個字節(jié)的數(shù)據(jù)時堡纬,就將自己的復制偏移量加上N;
- 從服務器每次收到主服務器傳播來的N字節(jié)數(shù)據(jù)時蒿秦,就將自己的復制偏移量加上N烤镐;
通過對比主從服務器的復制偏移量,可以很容易的發(fā)現(xiàn)主從數(shù)據(jù)庫是否一致:
15.4.2 復制積壓緩沖區(qū)
復制積壓緩沖區(qū)是主服務器維護的一個固定長度的FIFO隊列棍鳖,默認大小為1Mb炮叶;當主服務器在進行命令傳播時碗旅,它不僅會將寫命令發(fā)送給從服務器,還會將寫命令入隊到復制積壓緩沖區(qū)里面镜悉。
因此祟辟,復制積壓緩沖區(qū)會保持這一部分最近傳播的寫命令,并且賦值積壓緩沖區(qū)會為隊列中的每個字節(jié)記錄相應的復制偏移量侣肄。當斷線后從服務器重新連接上主服務器旧困,從服務器會通過PSYNC命令將自己的復制偏移量發(fā)送給主服務器,主服務器會根據(jù)這個偏移量來決定執(zhí)行何種同步:
- 如果offset偏移量之后的數(shù)據(jù)任然存在于復制積壓緩沖區(qū)里面稼锅,那么主服務器對從服務器執(zhí)行部分重同步操作吼具;
- 否則,就會執(zhí)行完整重同步操作矩距,這和SYNC命令是一樣的拗盒;
15.4.3 服務器運行ID
除了復制偏移量和復制積壓緩沖區(qū),還需要服務器運行ID锥债,才能實現(xiàn)PSYNC部分重同步功能锣咒;
每個Redis服務器,無論是主服務器還是從服務器赞弥,都會有一個自己的運行ID毅整;這個運行ID從服務器啟動時自動生成,由40個隨機的十六進制字符組成绽左。
當從服務器初次復制主服務器時悼嫉,主服務器會將自己的運行ID傳送給從服務器,而從服務器會將這個ID保存起來拼窥。當從服務器斷線并重新連接上主服務器時戏蔑,從服務器向當前連接的主服務器發(fā)送這個ID:如果主服務器發(fā)現(xiàn)這個ID和自己一樣,那么可以執(zhí)行部分重同步功能鲁纠,否則說明這是一個新的從服務器总棵,需要執(zhí)行完整重同步。
15.5 復制的實現(xiàn)
- 設置主服務器的地址和端口
- 和主服務器建立套接字連接
- 向主服務器發(fā)送PING命令
- 主服務器進行身份驗證
- 從服務器向主服務器發(fā)送端口信息
- 同步
- 命令傳播
- 心跳檢測(每秒一次)
- 檢測主從服務器的網(wǎng)絡連接狀態(tài)
- 輔助實現(xiàn)min-slaves配置改含,就是最少需要幾個slave(當然還可以配置如果所有的slave的延時都大于多少時)拒絕執(zhí)行寫命令情龄;
- 檢測命令丟失,在網(wǎng)絡傳輸中命令傳播可能丟失捍壤,通過offset主服務器就會發(fā)現(xiàn)這種問題骤视,將數(shù)據(jù)重傳;
第十六章 Sentinel
Sentinel(哨兵)是Redis高可用解決方案鹃觉,由一個或多個Sentinel實例組成的Sentinel系統(tǒng)可以監(jiān)視任意多個主服務器专酗,以及這些主服務器的從服務器,當監(jiān)視的主服務器下線時盗扇,自動將下線主服務器屬下的某個從服務器升級為新的主服務器祷肯。
當主服務器的下線時長超過用戶設定的閾值時沉填,Sentinel系統(tǒng)就會對主服務器進行故障轉移操作:
- 首先,Sentinel會挑選出一個故障主服務器下面的其中一個從服務器佑笋,并將這個從服務器升級為主服務器翼闹;
- 之后搂抒,Sentinel系統(tǒng)會向故障主服務器的所有從服務器發(fā)送新的復制命令馒疹,讓他們成為新的主服務器的從服務器,當所有的從服務器都開始復制新的主服務器時级及,故障轉移操作完畢颠锉;
- 另外法牲,Sentinel系統(tǒng)還會繼續(xù)監(jiān)視已下線的服務器,如果它重新上線琼掠,那么會讓他成為新的主服務器的從服務器拒垃;
16.1 啟動并初始化Sentinel
當一個Sentinel啟動時,他需要執(zhí)行以下步驟:
- 初始化服務器
Sentinel本質上只是一個運行在特殊模式下的Redis服務器瓷蛙,所以啟動Sentinel的第一步悼瓮,就是初始化一個普通的Redis服務器,但是不需要像普通Redis服務器那樣載入RDB文件之類的操作艰猬,因為它不需要使用Redis數(shù)據(jù)庫功能横堡;
將普通Redis服務器使用的代碼替換成Sentinel專用的代碼
初始化Sentinel狀態(tài)
根據(jù)給定的配置文件,初始化Sentinel的監(jiān)視主服務器列表
創(chuàng)建連向主服務器的網(wǎng)絡連接
16.2 獲取主服務器信息
Sentinel默認會以十秒一次的頻率通過命令連接向被監(jiān)視的主服務器發(fā)送INFO命令冠桃,通過分析主服務器返回的INFO命令回復命贴,Sentinel可以獲取以下兩方面的信息:
- 一方面是關于主服務器本身的信息,比如runID食听;
- 另一方面是關于該主服務器下的從服務器的信息胸蛛,根據(jù)這些信息,Sentinel就可以自動發(fā)現(xiàn)從服務器樱报;
16.3 獲取從服務器信息
Sentinel默認也會以十秒一次的頻率通過命令通道向從服務器發(fā)送INFO命令葬项,獲取從服務器的信息。
16.4 向主服務器和從服務器發(fā)送信息
在默認情況下迹蛤,Sentinel會以每兩秒一次的頻率民珍,通過命令連接,向所有被監(jiān)視的Redis服務器發(fā)送一個命令笤受,向服務器的sentinel:hello頻道發(fā)送一條信息穷缤;
16.5 接收來自被監(jiān)視服務器的頻道信息
Sentinel不僅會向所有被監(jiān)視的Redis服務器發(fā)送頻道信息,還會接收頻道信息箩兽,如果一個Redis服務器被多個Sentinel實例監(jiān)視,那么一個Sentinel向某個被監(jiān)視的Redis服務器發(fā)送的頻道信息章喉,會被其他所有監(jiān)視這個Redis服務器的Sentinel實例接收到汗贫,這些Sentinel接收到不是自己發(fā)送的頻道信息之后身坐,會對其他Sentinel發(fā)送的頻道信息進行解析;
16.5.1 更新sentinels字典
Sentinel為主服務器創(chuàng)建的sentinels字段保存了所有監(jiān)視這個Redis服務器的Sentinel的資料:
- sentinels字典的鍵是Sentinel的名字落包,格式為ip:port
- sentinels字典的值是對應的Sentinel實例結構
16.5.2 創(chuàng)建連向其他Sentinel的命令連接
當Sentinel通過頻道信息發(fā)現(xiàn)一個新的Sentinel時部蛇,不僅會在sentinels 字典中未該Sentinel創(chuàng)建相應的實例結構,還會創(chuàng)建一個連接到這個Sentinel的命令連接咐蝇,最終監(jiān)視同一個Redis服務器的所有Sentinel實例會成為一個網(wǎng)狀結構涯鲁;
16.6 檢測主觀下線狀態(tài)
默認情況下,Sentinel會以每秒一次的頻率向所有它創(chuàng)建了命令連接的實例(包括主從服務器有序,其他Sentinel)發(fā)送PING命令抹腿,并通過實例返回來判斷實例是否在線;
在Sentinel配置文件中有一個配置:down-after-milliseconeds旭寿,如果一個實例在down-after-milliseconeds毫秒后依然沒有返回有效的PING回應警绩,那么Sentinel就會判斷該實例為下線;
16.7 檢測客觀下線狀態(tài)
當Sentinel將一個主服務器判斷為主觀下線后盅称,為了確認這個服務器是否真的下線肩祥,它會詢問所有監(jiān)視這個服務器的Sentinel,看看其他Sentinel是否也認為這個服務器已經(jīng)下線缩膝,如果從其他Sentinel那里得到了足夠數(shù)量的下線判斷后(這個數(shù)量是配置的)混狠,Sentinel就會認為服務器為客觀下線,并會對該主服務器進行故障轉移操作疾层。不同的Sentinel對主服務器的下線判斷可能不一樣将饺,這個是因為不同的Sentinel配置可能是不一樣的。
16.8 選舉領頭Sentinel
當一個主服務器被判定為客觀下線時云芦,監(jiān)視這個下線主服務器的各個Sentinel會進行協(xié)商俯逾,選舉出一個領頭Sentinel,負責執(zhí)行故障轉移操作:
- 所有在線的Sentinel都有資格成為領頭Sentinel舅逸;
- 每次選舉之后桌肴,不論選舉是否成功,所有Sentinel的紀元都會增加1(一個計數(shù)器)
- 每個發(fā)現(xiàn)主服務器進入客觀下線的Sentinel都會要求別人選舉自己成為領頭
- 最想向目標Sentinel發(fā)送選舉自己要求的Sentinel將獲得選舉琉历,其他后來的選舉要求將被目標Sentinel拒絕坠七;
- 如果某個Sentinel被半數(shù)以上的Sentinel設置為leader,那么這個Sentinel將成為leader旗笔;
- 如果在給定的時間內沒有選舉出一個領頭Sentinel彪置,那么就會過一段時間再繼續(xù)選舉,直到產(chǎn)生leader蝇恶;
16.9 故障轉移
在選舉產(chǎn)生領頭Sentinel之后拳魁,領頭Sentinel就會對已下線的主服務器執(zhí)行故障轉移操作:
- 在已下線的主服務器的所有從服務器里,挑選出一個從服務器撮弧,并將其轉換為新的主服務器潘懊;
- 讓已下線的主服務器的所有從服務器改為復制新的主服務器姚糊;
- 將已下線服務器作為新的主服務器的從服務器,當這個舊的主服務器開始重新上線時授舟,他就會成為新的主服務器的從服務器救恨;
16.9.1 選出新的主服務器
故障轉移的第一步就是需要在故障的主服務器的所有從服務器中挑選一個狀態(tài)良好、數(shù)據(jù)完整的從服務器释树,然后向這個從服務器發(fā)送SLAVEOF no one命令肠槽,將這個從服務器轉換成主服務器;
領頭Sentinel會將已下線的Sentinel服務器的所有從服務器保存在一個列表里面奢啥,然后按照下面的規(guī)則過濾:
- 刪除列表中出于下線或者斷線狀態(tài)的服務器秸仙,保證剩余的從服務器都是正常在線的;
- 刪除列表中所有最近五秒內沒有回復過領頭Sentinel的INFO命令的從服務器扫尺,保證剩余的從服務器都是最近成功進行過通信的筋栋;
- 刪除所有與已下線服務器連接斷開超過 down-after-milliseconds * 10 毫秒的從服務器,保證剩余的從服務器沒有過早的與主服務器斷開連接正驻,也就是保證剩余的從服務器保存的數(shù)據(jù)都是比較新的弊攘;
之后,領頭Sentinel對列表中剩余的從服務器進行優(yōu)先級處理姑曙,取得優(yōu)先級最高的一個從服務器襟交,稱為最終被挑選出來的從服務器。
排序的規(guī)則是:
首先看服務器優(yōu)先級伤靠,然后是復制偏移量捣域,最后是服務器運行ID;
16.9.2 修改從服務器的復制目標
16.9.3 將舊的主服務器變?yōu)閺姆掌?/h3>
最后宴合,需要注意的是焕梅,Sentinel只會和主服務器和從服務器創(chuàng)建命令連接和訂閱連接,Sentinel之間則只創(chuàng)建命令連接卦洽。
第十七章 集群
Redis集群是Redis提供的分布式解決方案贞言,復制(replication)提供了主從數(shù)據(jù)庫方案,Sentinel提供了高可用(故障轉移)方案阀蒂,而Redis集群則在此基礎上提供了負載均衡(通過分片)的分布式方案该窗。
集群主要包括下面一些內容:節(jié)點、槽指派蚤霞、命令執(zhí)行酗失、重新分片、轉向昧绣、故障轉移规肴、消息等;
17.1 節(jié)點
一個Redis集群由多個節(jié)點組成(node),在剛開始的時候奏纪,每個節(jié)點都是相互獨立的鉴嗤,他們都處于一個只包含自己的集群當中斩启,我們可以通過CLUSTER MEET <ip> <port> 命令來完成這個工作序调,當向一個節(jié)點執(zhí)行這個命令時,可以讓當前節(jié)點將ip和port所代表的節(jié)點添加到當前集群中兔簇;
一個節(jié)點就是一個運行在集群模式的Redis服務器发绢,Redis服務器會在啟動的時候根據(jù)cluster-enbale配置是否為yes來確定是否開啟集群模式;集群模式的Redis服務器依然會繼續(xù)使用單機模式下的Redis服務器組件垄琐,比如復制等功能边酒;
clusterNode結構是一個節(jié)點的數(shù)據(jù)結構:
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é)點的視角下集群目前所處的狀態(tài)。
17.1.1 CLUSTER MEET命令的實現(xiàn)
通過向節(jié)點A發(fā)送CLUSTER MEET命令狸窘,可以讓節(jié)點A將另外一個節(jié)點B添加到節(jié)點A當前所在的集群里面墩朦,收到命令的節(jié)點A將于節(jié)點B進行握手,以此來確認彼此的存在:
- (1) 節(jié)點A會為節(jié)點B創(chuàng)建一個clusterNode結構翻擒,并將該結構添加到自己的clusterState.nodes字典里面氓涣;
- (2)之后,節(jié)點A將給節(jié)點B發(fā)送一條MEET消息陋气;
- (3)節(jié)點B接收到節(jié)點A發(fā)送的MEET消息劳吠,B會為A創(chuàng)建一個clusterNode結構,并將該結構添加到自己的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消息,握手結束番川;
- (8)之后到涂,節(jié)點A會將B的信息通過Gossip協(xié)議傳播給集群中的其他節(jié)點,讓其他節(jié)點也與B握手颁督,最終践啄,經(jīng)過一段時間,節(jié)點B會被集群中的所有節(jié)點認識沉御;
17.2 槽指派
Redis集群通過分片的方式來保存數(shù)據(jù)庫中的鍵值對屿讽,集群的整個數(shù)據(jù)庫被分為16384(2^14)個槽(slot),數(shù)據(jù)庫中的每個鍵都屬于這16384個槽中的一個,集群中的每個節(jié)點可以處理0個或最多16384個槽伐谈;
當數(shù)據(jù)庫中的16384個槽都被處理時烂完,集群狀態(tài)出于上線狀態(tài)(ok),如果數(shù)據(jù)庫中任意一個槽沒有被處理诵棵,那么集群出于下線狀態(tài)(fail)抠蚣;
可以通過命令 CLUSTER ADDSLOTS命令將一個或者多個槽指派給某個節(jié)點負責;
節(jié)點的clusterNode結構的slots 屬性和 numslots 記錄了節(jié)點負責處理那些槽履澳,slots屬性是一個二進制位數(shù)組嘶窄,這個數(shù)組的長度是16384 / 8 = 2048個字節(jié),一共包含16384個二進制位距贷。
Redis以0為起始索引柄冲,16383為終止索引,對slots數(shù)組中的16384個二進制位進行編號忠蝗,并根據(jù)第i位上的二進制值來判斷槽是否是該節(jié)點負責现横;
一個節(jié)點會將自己負責的槽信息發(fā)送給其他集群中的節(jié)點,以此來告訴其他節(jié)點自己負責哪些槽阁最;節(jié)點A通過消息接收到節(jié)點B負責的槽信息戒祠,會在A節(jié)點自己的clusterState.nodes字典里面找到B對應的clusterNode結構,并對其中的slots屬性進行更新闽撤,因為集群中的每個節(jié)點都會將自己的slots數(shù)組發(fā)送給其他節(jié)點得哆,所以集群中的所有節(jié)點都知道16384個槽都是由哪些節(jié)點負責的;
這里需要注意的一點是哟旗,如果只將16384個槽記錄在clusterNode的slots數(shù)組里贩据,那么如果需要知道第i個槽是哪個節(jié)點負責的,就需要遍歷clusterState.nodes字典中的所有clusterNode.slots數(shù)組闸餐,這個負責度為O(N)饱亮,所以,clusterState.slots數(shù)組會保存每一個槽是由哪個節(jié)點負責的舍沙。這樣的話近上,如果想要檢測槽i是否已經(jīng)被指派,只需要檢測clusterState.slots數(shù)組的第i個項即可拂铡;
當然壹无,是否只需要在clusterState.slots數(shù)組就可以了呢?clusterNode.slots是否還需要呢感帅?答案是需要的斗锭,因為節(jié)點需要將自己負責的slots傳播給其他節(jié)點,現(xiàn)在只需要將clusterNode.slots發(fā)送出去即可失球,但是如果沒有clusterNode.slots岖是,就需要在clusterStste.slots里面遍歷,找出所有某個節(jié)點負責的slot,然后再發(fā)送給其他節(jié)點豺撑,這就有點麻煩了烈疚;
17.3 在集群中執(zhí)行命令
在對集群中的16384個槽都指派完成之后,集群就進入上線狀態(tài)聪轿,這時客戶端就可以向集群發(fā)送數(shù)據(jù)命令了爷肝;
當客戶端向節(jié)點發(fā)送數(shù)據(jù)庫相關命令時,接收到命令的節(jié)點會計算出命令要處理的數(shù)據(jù)庫鍵屬于哪個槽:
- 如果這個槽是自己負責的屹电,那么節(jié)點直接執(zhí)行這個命令阶剑;
- 如果鍵所在的槽不是當前節(jié)點負責的,那么節(jié)點會向客戶端返回一個MOVED錯誤危号,指引客戶端轉向正確的節(jié)點執(zhí)行命令;
節(jié)點首先需要計算出鍵所對應的槽素邪,Redis使用CRC16(key) & 16383來計算這個值外莲,當節(jié)點計算出鍵所對應的槽之后,節(jié)點就會檢查自己在數(shù)組clusterState.slots的第i項是否是自己兔朦,如果是偷线,則說明這個鍵是由自己負責的,否則沽甥,會指引客戶端轉向clusterState.slots[i]所指向的節(jié)點執(zhí)行命令声邦;
需要注意的是,節(jié)點只能使用0號數(shù)據(jù)庫摆舟,而單機則沒有這個限制亥曹;
17.4 重新分片
Redis集群的重新分片操作可以將任意已經(jīng)分配給某個節(jié)點的槽改為指派給其他的另外一個節(jié)點,重新分片操作可以在線進行恨诱,不需要下線媳瞪,源節(jié)點和目標節(jié)點都可以繼續(xù)處理命令請求;
Redis集群的重新分片操作由Redis的集群管理軟件redis-trib執(zhí)行照宝,redis-trib對集群的單個槽slot進行重新分片的步驟如下:
- (1)redis-trib向目標節(jié)點發(fā)送CLUSTER SETSLOT <slot> IMPORTING <source_id>命令蛇受,讓目標節(jié)點準備好從源節(jié)點導入屬于槽slot的鍵值對;
- (2)redis-trib對源節(jié)點發(fā)送CLUSTER SETSLOT <slot> MIGRATING <target_id>命令厕鹃,讓源節(jié)點準備好將屬于槽slot的鍵值對遷移到目標節(jié)點兢仰;
- (3)redis-trib向源節(jié)點發(fā)送CLUSTER GETKEYSINSLOT <slot> <count>命令,獲取最多count個屬于槽slot的鍵值對的鍵名字剂碴;
- (4)對于步驟3得到的每個鍵把将,redis-trib都將向源節(jié)點發(fā)送命令,原子的將這些鍵遷移到目標節(jié)點汗茄;
- (5)重復步驟3和4秸弛,直到源節(jié)點保存的所有屬于槽slot的鍵值對都被遷移到目標節(jié)點為止;
- (6)redis-trib會向集群中的任意一個節(jié)點發(fā)送CLUSTER SETSLOT <slot> NODE <taregt_id>命令,將槽slot指派給目標節(jié)點递览,這一信息會通過消息發(fā)送給集群中的所有節(jié)點叼屠,最終集群中的所有節(jié)點都將知道槽slot已經(jīng)指派給了目標節(jié)點;
17.5 ASK錯誤
在進行重新分片的過程中绞铃,因為集群正常在線镜雨,所以可能出現(xiàn)一種情況,屬于被遷移槽的一部分鍵在源節(jié)點中儿捧,而另一部分已經(jīng)遷移到目標節(jié)點荚坞,這時候如果客戶端向源節(jié)點發(fā)送一個數(shù)據(jù)庫操作命令,而這個被操作的鍵剛好在被遷移的鍵中時:
- 源節(jié)點會現(xiàn)在自己的數(shù)據(jù)庫中查找給定的鍵菲盾,如果找到了颓影,那么就直接處理;
- 如果沒找到懒鉴,那么源節(jié)點會向客戶端返回一個ASK錯誤诡挂,指引客戶端向目標節(jié)點發(fā)起請求;
ASK錯誤和MOVED錯誤的區(qū)別:
- MOVED錯誤表示槽的負責權已經(jīng)從一個節(jié)點轉向另外一個節(jié)點了临谱,所以在客戶端收到MOVED錯誤之后璃俗,客戶端每次都可以使用轉向后的節(jié)點;
- ASK錯誤是一種臨時方案悉默,是一次性的城豁,ASK錯誤之后應該會出現(xiàn)一次MOVED錯誤;
17.6 復制與故障轉移
Redis集群的節(jié)點分為主節(jié)點(master)和從節(jié)點(slave)抄课,主節(jié)點負責處理槽唱星,而從節(jié)點負責復制某個主節(jié)點,并在主節(jié)點下線時代替主節(jié)點剖膳;
向一個節(jié)點發(fā)送CLUSTER REPLICATION <node_id>魏颓,可以讓收到命令的節(jié)點對主節(jié)點進行復制:
- 接收到命令的節(jié)點首先會在自己的clusterState.modes字典中找到主節(jié)點的clusterNode結構,然后將自己的clusterState.myself.slaveof指針指向這個clusterNode吱晒,以此來表示當前節(jié)點正在復制這個主節(jié)點甸饱;
- 然后會修改自己的clusterState.myself.flags中的屬性,關閉原本的master屬性仑濒,打開slave屬性叹话,表示節(jié)點變成一個從節(jié)點;
- 然后墩瞳,會復用復制的代碼對master節(jié)點進行復制驼壶;
一個節(jié)點成為從節(jié)點,并開始復制某個主節(jié)點這個消息會被集群的其他節(jié)點知道喉酌,集群中的所有節(jié)點都會在代表主節(jié)點的clusterNode.slaves屬性中記錄正在復制該節(jié)點的從節(jié)點信息热凹;
集群中的每個節(jié)點都會定期向其他節(jié)點發(fā)送PING消息泵喘,以此來檢測對方是都還在線,如果接收PING消息的節(jié)點沒有在規(guī)定時間內返回PONG消息般妙,那么發(fā)送PING消息的節(jié)點就會將接受PING消息的節(jié)點標記為疑似下線(probable fail 纪铺,PFAIL);
集群中的每個節(jié)點都會通過互相發(fā)送消息來交互集群各個節(jié)點的狀態(tài)信息碟渺,如果一個集群里面超過半數(shù)以上負責處理槽的主節(jié)點將某個節(jié)點標記為疑似下線狀態(tài)鲜锚,那么這個主節(jié)點將被標記為下線,將這個主節(jié)點標記為下線的節(jié)點將會向集群廣播這個消息苫拍,所有收到這條消息的節(jié)點都會立即標記這個節(jié)點已下線芜繁。
當一個從節(jié)點發(fā)現(xiàn)自己復制的主節(jié)點已下線時,從節(jié)點開始對下線主節(jié)點進行故障轉移操作:
- (1)在所有復制主節(jié)點的從節(jié)點里面會有一個從節(jié)點被選中绒极;
- (2)選中的從節(jié)點執(zhí)行SLAVE no one命令骏令,成為新的主節(jié)點;
- (3)新的主節(jié)點會認領所有下線主節(jié)點的槽指派集峦;
- (4)新的主節(jié)點向集群廣播一條PONG消息伏社,集群中的其他節(jié)點會基于這條消息知道這個節(jié)點成為了新的主節(jié)點,并接管了原來主節(jié)點下的所有從節(jié)點塔淤;
- (5)新的主節(jié)點開始處理自己負責的槽的相關命令,故障轉移完成速妖;
選舉新的主節(jié)點的步驟為:
- (1)在每次選舉中高蜂,負責處理槽的主節(jié)點有一次投票機會;
- (2)當從節(jié)點發(fā)現(xiàn)自己正在復制的主節(jié)點已下線時罕容,它會向集群廣播一條消息备恤,要求所有具備選舉權的節(jié)點給自己投票;
- (3)接收到這條消息的節(jié)點锦秒,如果是負責處理槽的主節(jié)點露泊,并且自己還沒有投票,那么就會給這個節(jié)點投票旅择;
- (4)如果一個從節(jié)點收集到大于集群主節(jié)點數(shù)量一半的票數(shù)惭笑,那么這個從節(jié)點就選舉為新的主節(jié)點;
- (5)如果一個紀元里面沒有從節(jié)點獲得的票數(shù)符合要求生真,那么就會進入下一個紀元沉噩;
17.7 消息
消息類型:MEET PING PONG FAIL PUBLISH
gossip
redis cluster something
第十九章 事務
Redis通過MULTI、EXEC柱蟀、WATCH等命令實現(xiàn)事務功能川蒙,事務提供了一種將多個命令請求打包,然后一次性的长已,按順序的執(zhí)行多個命令的機制畜眨,并且在事務執(zhí)行期間昼牛,服務器不會中斷事務而去執(zhí)行其他客戶端的請求,它會將事務中的命令都執(zhí)行完成康聂,然后才去執(zhí)行其他客戶端的命令贰健;
19.1 事務
一個事務從開始到結束通常會經(jīng)歷以下三個階段:
- 事務開始
MULTI命令可以讓執(zhí)行該命令的客戶端從非事務狀態(tài)切換到事務狀態(tài),這一切換是通過在客戶端狀態(tài)的flags屬性打開REDIS_MULTI標志來完成早抠;
- 命令入隊
如果一個客戶端處于非事務狀態(tài)時霎烙,那么這個客戶端發(fā)送的命令就會立即被執(zhí)行,否則就會有不同的執(zhí)行路徑:
- 如果客戶端發(fā)送的命令是EXEC DISCARD WATCH MULTI四個命令中的一個蕊连,那么服務器立即執(zhí)行這個命令悬垃;
- 否則,就會將客戶端的命令放到一個事務隊列里面甘苍,然后給客戶端返回QUEUED回復尝蠕;
- 事務執(zhí)行
當一個處于事務狀態(tài)的客戶端向服務器發(fā)送EXEC命令時,這個命令將被立即執(zhí)行载庭,服務器會遍歷這個客戶端的事務隊列看彼,執(zhí)行隊列里面保存的所有命令,最后將執(zhí)行命令的結果全部返回給客戶端囚聚。
19.2 WATCH命令的實現(xiàn)
WATCH是一個樂觀鎖靖榕,它可以在EXEC執(zhí)行前,監(jiān)視任意數(shù)量的數(shù)據(jù)庫鍵顽铸,并在EXEC執(zhí)行時茁计,檢查被監(jiān)視的鍵是否至少有一個已經(jīng)被修改過了,如果是的話谓松,服務器將拒絕執(zhí)行事務星压,并向客戶端返回事務執(zhí)行失敗的空回復。
19.3 Redis事務的ACID性質
19.3.1 A(原子性)
原子性指的是數(shù)據(jù)庫事務將多個操作當做一個整體執(zhí)行鬼譬,要么都執(zhí)行娜膘,要么一個也不執(zhí)行,對于Redis來說优质,要么全部執(zhí)行Redis事務隊列中的所有命令竣贪,要么一個也不執(zhí)行,所以具備原子性盆赤;
需要注意的是贾富,Redis的事務與傳統(tǒng)關系型數(shù)據(jù)庫事務的最大區(qū)別就是Redis不支持事務回滾,即使事務隊列中的某個命令在執(zhí)行時出錯牺六,事務也不會停止颤枪,會繼續(xù)執(zhí)行下去,直到將事務中的命令全部執(zhí)行完成淑际;
19.3.1 C(一致性)
一致性是指畏纲,如果數(shù)據(jù)庫在執(zhí)行事務之前是一致的扇住,那么事務執(zhí)行之后也應該是一致的,無論事務是否執(zhí)行成功盗胀。一致是指數(shù)據(jù)符合數(shù)據(jù)庫本身的定義艘蹋,沒有包含非法或者無效的錯誤數(shù)據(jù):
- 入隊錯誤:如果命令在入隊的時候出現(xiàn)了比如命令不存在等錯誤,那么Redis將拒絕執(zhí)行這個事務票灰;
- 執(zhí)行錯誤女阀,執(zhí)行錯誤一般是對數(shù)據(jù)庫鍵執(zhí)行了錯誤的類型操作導致,這種錯誤會被Redis識別并進行錯誤處理屑迂,這些命令不會對數(shù)據(jù)庫進行任何修改浸策,所以不會造成影響
- 服務器停機:服務器停機之后,如果開啟了Redis持久化惹盼,那么服務器重啟之后會將數(shù)據(jù)庫狀態(tài)還原到一個一致的狀態(tài)庸汗,如果沒有開啟持久化,那么Redis重啟之后數(shù)據(jù)庫是空白的手报,是一致的狀態(tài)蚯舱;
19.3.1 I (隔離性)
事務的隔離性是指,即使數(shù)據(jù)庫中多個事務并發(fā)的執(zhí)行掩蛤,各個事務之間也不會互相影響枉昏,并且在并發(fā)狀態(tài)下執(zhí)行的事務和串行執(zhí)行的事務產(chǎn)生的結果一致;
對于Redis來說揍鸟,它使用單線程的方式執(zhí)行事務(以及事務隊列中的命令)凶掰,并且服務器保證,在執(zhí)行事務期間蜈亩,不會對事務進行中斷,因此前翎,Redis事務總是以串行的方式執(zhí)行的稚配;
19.3.1 D(耐久性)
事務的耐久性是指,當一個事物執(zhí)行完畢時港华,執(zhí)行這個事務所得的結果已被保存到永久存儲介質里面了道川,即使服務器停機,執(zhí)行事務的結果也不會丟失立宜。
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: 設定過期時間冒萄,單位為秒
- PX milliseconds: 設定過期時間,單位為毫秒
- NX: 僅當key不存在時設置值
- XX: 僅當key存在時設置值
- (3) RedLock
在Redis的分布式環(huán)境中橙数,我們假設有N個Redis master尊流。這些節(jié)點完全互相獨立,不存在主從復制或者其他集群協(xié)調機制灯帮。我們確保將在N個實例上使用與在Redis單實例下相同方法獲取和釋放鎖⊙录迹現(xiàn)在我們假設有5個Redis節(jié)點逻住,同時我們需要在5臺服務器上面運行這些Redis實例,這樣保證他們不會同時都宕掉迎献。
為了取到鎖瞎访,客戶端應該執(zhí)行以下操作:
- 獲取當前Unix時間,以毫秒為單位吁恍。
- 依次嘗試從5個實例扒秸,使用相同的key和具有唯一性的value(例如UUID)獲取鎖。當向Redis請求獲取鎖時冀瓦,客戶端應該設置一個網(wǎng)絡連接和響應超時時間伴奥,這個超時時間應該小于鎖的失效時間。例如你的鎖自動失效時間為10秒咕幻,則超時時間應該在5-50毫秒之間渔伯。這樣可以避免服務器端Redis已經(jīng)掛掉的情況下,客戶端還在死死地等待響應結果肄程。如果服務器端沒有在規(guī)定時間內響應锣吼,客戶端應該盡快嘗試去另外一個Redis實例請求獲取鎖。
- 客戶端使用當前時間減去開始獲取鎖時間(步驟1記錄的時間)就得到獲取鎖使用的時間蓝厌。當且僅當從大多數(shù)(N/2+1玄叠,這里是3個節(jié)點)的Redis節(jié)點都取到鎖,并且使用的時間小于鎖失效時間時拓提,鎖才算獲取成功读恃。
- 如果取到了鎖,key的真正有效時間等于有效時間減去獲取鎖所使用的時間(步驟3計算的結果)代态。
- 如果因為某些原因寺惫,獲取鎖失敗(沒有在至少N/2+1個Redis實例取到鎖或者取鎖時間已經(jīng)超過了有效時間)蹦疑,客戶端應該在所有的Redis實例上進行解鎖(即便某些Redis實例根本就沒有加鎖成功西雀,防止某些節(jié)點獲取到鎖但是客戶端沒有得到響應而導致接下來的一段時間不能被重新獲取鎖)。
無論如何歉摧,在解鎖的時候需要check設置的value艇肴,這應該是一個唯一的值,不是每個客戶端都可以解鎖叁温,只有設置這個可以的客戶端才能解鎖再悼。