redis內存k-v,支持多種數據結構,第一個重點在於如何操做更快和適當的節省內存,第二個重點在於分佈式管理。本文redis基於3.0。第一部分將介紹全部內存數據結構實現,關注rehash的實現,對編寫內存存儲提供數據結構參考沒什麼框架,單線程,無內存池等複雜設計,基本不支持正規的ACID;還會介紹內存溢出淘汰策略,過時鍵刪除,持久化等功能;第二部分將介紹分佈式集羣,redis自身的主從模式/哨兵模式/集羣模式,經常使用的codis,公司自研的非開源集羣。順便說了些雙機房中redis同步方案,redis應用中應優化的點以及常見的redis熱key解決方案。前端
對象類型 | 編碼方式 | 選擇條件 | 編碼詳情 |
string | int | long類型整數 | ptr直接指向整數 |
---|---|---|---|
embstr動態字符串 | 長度<=44 | 數組形式組織sds,len/內存預分配/結尾有\0 | |
動態字符串 | 長度>44 | 鏈表形式組織sds | |
列表 | 壓縮列表 | 長度<64&&元素數<512 | 數組形式組織ziplist |
雙端鏈表 | 長度>=64&&元素數>=512 | 雙端鏈表 | |
quicklist | 3.2版本後 | xx | |
哈希 | 壓縮列表 | 長度<64&&元素數<512 | |
字典 | 長度>=64&&元素數>=512 | 兩個table/若干桶 | |
集合 | 整數集合 | 元素數<512 | |
字典 | 元素數>=512 | ||
有序集合 | 壓縮列表 | 長度<64&&元素數<128 | 分支最小元素/分值 |
跳錶 | 長度>=64&&元素數>=128 | 字典+跳錶 |
保持0仍然可使用部分C語言字符串的一些函數
Len 獲取長度,保證二進制安全;
多出剩餘空間,每次檢查free預分配內存,杜絕緩衝區溢出,惰性釋放,減小修改字符串帶來的內存重分配次數node
struct sdshdr { len = 11; free = 0; buf = "hello world\0"; // buf 的實際長度爲 len + 1 }; 分配內存,刪除才釋放 # 預分配空間足夠,無須再進行空間分配 if (sdshdr.free >= required_len): return sdshdr # 計算新字符串的總長度 newlen = sdshdr.len + required_len # 若是新字符串的總長度小於 SDS_MAX_PREALLOC # 那麼爲字符串分配 2 倍於所需長度的空間 # 不然就分配所需長度加上 SDS_MAX_PREALLOC (1M)數量的空間 if newlen < SDS_MAX_PREALLOC: newlen *= 2 else: newlen += SDS_MAX_PREALLOC # 分配內存 newsh = zrelloc(sdshdr, sizeof(struct sdshdr)+newlen+1)
壓縮列表使用特殊的編碼來標識長度,再加上連續的內存,很是節約空間mysql
area |<----------------------------------------------- entry ----------------------->| size 5 byte 2 bit 6 bit 11 byte +-------------------------------------------+----------+--------+---------------+ component | pre_entry_length | encoding | length | content | | | | | | value | 11111110 00000000000000000010011101100110 | 00 | 001011 | hello world | +-------------------------------------------+----------+--------+---------------+
Pre_entry_length
1 字節:若是前一節點的長度小於 254 字節,便使用一個字節保存它的值。
5 字節:若是前一節點的長度大於等於 254 字節,那麼將第 1 個字節的值設爲 254 ,而後用接下來的 4 個字節保存實際長度。
encodinng/length/content
以 00 、 01 和 10 開頭的字符數組的編碼方式以下:redis
編碼 | 編碼長度 | content 部分保存的值 |
---|---|---|
00bbbbbb | 1 byte | 長度小於等於 63 字節的字符數組。 |
01bbbbbb xxxxxxxx | 2 byte | 長度小於等於 16383 字節的字符數組。 |
10____ aaaaaaaa bbbbbbbb cccccccc dddddddd | 5 byte | 長度小於等於 4294967295 的字符數組。 |
具體如何省內存:相好比雙向,指針加sds的len,free結尾空,2*4+1+2*4(32位指針和Int都是4字節);壓縮鏈表2/6字節。算法
添加節點在前面,要更新pre_entry_length,next 的 pre_entry_length 只有 1 字節長,但編碼 new 的長度須要 5 字節的時候可能連鎖更新。next 的 pre_entry_length 有 5 字節長,但編碼 new 的長度只須要 1 字節不作處理。sql
這裏的encoding是針對整個intset的。當某元素長度超過期要總體升級編碼方式。全存Int所以不須要length。只會升級不會降級。升級過程:數據庫
擴展內容。從後開始移動,將新值插入 bit 0 15 31 47 63 95 127 value | 1 | 2 | 3 | ? | 3 | ? | | ^ | | +-------------+ int16_t -> int32_t
相比於平衡二叉樹,不須要嚴格的平衡,隨機層數.插入和刪除不須要調整性能很高查找略遜色
https://www.cl.cam.ac.uk/teac...後端
int zslRandomLevel(void) { int level = 1; while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF)) //這裏取小於0xffff的數,有0.25的機率level+1,所以level有1/4機率爲2, 1/16的機率爲3等等 level += 1; return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL; } ZSKIPLIST_MAXLEVEL=32 ZSKIPLIST_P=1/4 一個節點的平均層數 = 1/(1-p),Redis 每一個節點平均指針數爲1.33 平均時間複雜度:O(logn)
若進行了rehash,先遍歷小hash表的v & t0->sizemask索引指向的鏈表,再遍歷大hash表中該索引rehash後的全部索引鏈表。
由於sizemask=sizehash-1所以低位全是1,索引取決於hashkey的低K位,
同一個節點的hashkey不變,若原來爲8位hash,hashkey爲…abcd,原索引計算爲bcd,
擴展到16位hash,索引變爲abcd,若要找出全部原bcd索引的鏈表,須要在新的hash中找0bcd,1bcd。
由於要循環高位,因此這樣從高位到低位反向來,例如:
000 --> 100 --> 010 --> 110 --> 001 --> 101 --> 011 --> 111 --> 000
0000 --> 1000 --> 0100 --> 1100 --> 0010 --> 1010 --> 0110 --> 1110 --> 0001 --> 1001 --> 0101 --> 1101 --> 0011 --> 1011 --> 0111 --> 1111 --> 0000
當rehash時,可能會有重複,但不會有遺漏數組
do { /* Emit entries at cursor */ de = t1->table[v & m1]; while (de) { fn(privdata, de); de = de->next; } /*這裏v從0開始,加1只取前m1-m0位,再與後m0位合併*/ v = (((v | m0) + 1) & ~m0) | (v & m0); /* Continue while bits covered by mask difference is non-zero */ } while (v & (m0 ^ m1)); //這裏異或前m1-m0位全是1,直到while中的v爲全1後加1變爲全0這裏爲0退出。所以若原來v爲110,8到32,rehash的表將遍歷00110,01110,10110,11110 而後是下一個v的確認 v |= ~m0; //m0低位全是0,>m0全是1,將超出m0的置1,只保留低m0位 v = rev(v); //二進制翻轉 v++; //加1,正常進位 v = rev(v); //二進制翻轉,這步以後至關於將v從高位+1向低位進位
Redis 用來當作LRU cache的幾種策略(使用內存已達到maxmemory):緩存
noeviction:無策略,直接返會異常
allkeys-lru:全部key進行LRU,先移除最久使用的(當前時間,減去最近訪問的時間)
allkeys-random:隨機移除
volatile-random:只隨機移除有過時時間的key
volatile-tt: 優先移除最短ttl的有過時時間的key
近似的LRU。採樣逐出(默認5個裏淘汰一個)。https://redis.io/topics/lru-c...
4.0後引入LFU(least frequently):大概原理是次數達到一個階段給個計數器初始值,隨時間遞減。採樣取最小淘汰(源碼LFULogIncr)
接收到SLAVEOF命令執行步驟:
設置masterhost,masterport 發送OK給客戶端 建立socket connect到主服務器,主服務器accept 發送ping給主服務器,收到PONG繼續不然斷開重連 主服務器requirepass,從服務器masterauth 發送端口給主服務器 REPLCONF listening-pot <port-number> 同步SYNC/PSYNC 命令傳播
1.SYNC
主服務器BGSAVE命令生成一個RDB文件,並使用緩衝區開始記錄寫命令
BGSAVE結束後後發送RDB文件給從服務器
從服務器載入
主服務器將和緩衝區中寫命令發送給從服務器,從服務器執行
2.命令傳播
主服務器將全部寫命令傳播給從服務器
每秒一次頻率向主服務器發送REPLCONF ACK <replication_offset>進行心跳檢測。檢測網絡和命令丟失
主服務器配置min-slaves-to-write n, min-slaves-max-lag m當從服務器數量少於3個,或者延遲大於等於10將拒絕執行寫命令
根據replication_offset檢測是否丟失命令,補發命令
3.斷線後重複製的優化 PSYNC
2.8版本以上redis使用PSYNC命令代替SYNC,斷線後使用部分重同步,其餘使用SYNC
從服務器向主服務器發送命令:首次PSYNC ? -1 ,斷線後重複製 PSYNC <runid> <offset>。主服務器返回:+FULLERSYNC <runid> <offset> ,+CONTINUE , -ERR沒法識別從服務器重發SYNC命令
4.上面2/3都是2.8以上才支持,須要用到replication_offset,複製積壓緩衝區,服務運行ID
主服務器每次向從服務器傳播N個字節,將本身的複製偏移量加N。從服務器每次收到N個字節,將本身的複製偏移量加N 主服務器進行命令傳播時,不只會將寫命令發送給從服務器,還會將寫命令寫入複製積壓緩衝區,先進先出 從服務器會記錄正在複製的主服務器的運行ID,網絡斷開後,從服務器向主服務器發送這個ID,主服務器根據本身運行ID決定是部分重同步仍是徹底同步
哨兵系統也是一個或多個特殊的redis服務器,監視普通服務器,負責下線主服務器和故障轉移
1.啓動
(1)初始化服務器
sentinel不適用數據庫,再也不如RDB/AOF
(2)將普通redis服務器使用的代碼替換成sentinel專用代碼
使用不一樣端口,命令集(只有PING,SENTINEL,INFO.SUBSCRIBE,UNSUBSCRIBE,PSUBSCRIBE,PUNSUBSCRIBE)
(3)初始化sentinel狀態
(4)根據給定的配置文件,初始化sentinel的監視主服務器列表
(5)建立連向主服務器的網絡鏈接
命令鏈接,訂閱鏈接(在創建後發送SUBSCRIBE __sentinel__:hello,sentinel需求經過接收其餘服務器發來的頻道信息發現未知的sentinel)
2.獲取主服務器信息
sentinel默認10s一次向主服務器發INFO命令,獲取更新sentinelRedisInstance的run_id,role,slaves的等
3.獲取從服務器信息
sentinel會對主服務器的從服務器創建命令鏈接和訂閱鏈接,也是10s/次發送INFO,更新slaves的sentinelRedisInstance
4.向主服務器和從服務器發送信息
sentinel默認2s/次用命令鏈接向主服務器和從服務器發送 PUBLISH __sentinel__:hello "<s_ip>,<s_port>,<s_runid>,<s_epoch>,<m_name>,<m_ip>,<m_port>,<m_epoch>"
對每一個與sentinel鏈接的服務器,即發送信息到頻道又訂閱頻道接收信息。收到信息後提取參數檢查如果本身的丟棄,不然根據信息更新主服務sentinelRedisInstance中的sentinels,建立鏈接向其餘sentinel的命令鏈接
5.檢測主觀下線狀態
sentinel默認1s/次的頻率向全部主/從/sentinel服務器發送PING命令,有效回覆爲+PONG,-LOADING,-MASTERDOWN。當一個實例在down-after-milliseconds內,連續向sentinel返回無效回覆,sentinel修改實例中flags加入|SRI_S_DOWN標識主觀下線
6.檢查客觀下線狀態
若是被sentinel判斷爲主觀下線,sentinel當前配置紀元爲0,將向其餘sentinel發送命令 SENTINEL is-master-down-by-addr <ip> <port> <current_epoch> <runid>
返回
<down_state> <leader_runid> // *表明命令僅用於檢測主服務器的下線狀態 <leader_epoch> //前一個只爲*則爲0
當接收到認爲下線的sentinel數量超過quorum(sentinel moniter 127.0.0.1 6379 2中2設置)則flags加|SRI_O_DOWN
7.選舉領頭Sentinel(raft)
也經過SENTINEL is-master-down-by-addr 看來是要分開進行,帶runid。
每一個發現主服務器進入客觀下線的sentinel向其餘sentinal發送命令
在一個配置epoch中將先到的設爲局部領頭,不能再更改。
接收回複檢查epoch的值和本身的相同就取出leader_runid,若是發現本身被半數以上選擇,則成爲領頭,epoch+1
若是在規定時間內未選舉成功,epoch+1從新選舉
8.故障轉移
領頭進行故障轉移
1) 選出新的主服務器
在線的,5s內回覆過INFO的,與主服務器斷開鏈接時間足夠短,優先級高,複製偏移量大,runid最小 發送SLAVEOF no one 以1s/次(其餘是10s/次)的頻率向該服務器發送INFO。當role變爲master時繼續2
2) 向下線的主服務的其餘從服務器發送SLAVEOF命令
3) 向舊的主服務器發送SLAVEOF命令
一個集羣由多個node組成,經過分片進行數據共享,CLUSTER MEET <ip> <port>將各階段加入到cluster
1.啓動
一個node就是運行在集羣模式下的redis服務器,在啓動時若cluster-enabled是yes,則開啓服務器的集羣模式。
節點繼續使用單機模式的服務器組件,只是serverCron函數會調用集羣模式特有的clusterCron函數,執行集羣的常規操做,例如向集羣的其餘節點發送Gossip消息,檢查節點是否斷線,或者檢查是否須要對下線的節點進行故障轉移操做等。節點數據庫和單機徹底相同除了智能使用0號出具庫這和個限制,另外除了將鍵值對保存在數據庫裏邊以外,節點還會用clusterState中的slots_to_keys跳躍表來保存槽和鍵,方便對屬於某槽全部數據庫鍵進行批量操做
2.客戶點向A發送CLASTER MEET <B.ip> <B.port>
A建立B的clusterNode加入到clusterState.nodes中
發送MEET給B
B返回PONG
A發送PING,握手完成
A將B的信息經過Gossip傳播給急羣衆其餘節點
3.槽指派,向節點發送CLUSTER ADDSLOTS <slot> [slot ...]
遍歷全部輸入槽,若是有已經指派的返回錯誤,若是都沒有指派,再遍歷一次:
更新當前lusterState.slots[i]設爲Myself
更新本身clusterNode 的slots,numslots屬性
將本身的slots數組經過消息發送給集羣中其餘節點,A收到B後會把本身的clusterState.nodes中查找B對應的clusterNode結構,更新其中的slots數組;更新clusterNode中的slots,numslots屬性
維護總體slots目的:查某個槽被哪一個節點處理
維護單個節點slots目的:將某節點的全部槽指派信息發送給其餘。
4.執行命令
在全部的槽都指派完畢以後,集羣就會進入上線狀態,這是客戶端就能夠向集羣中的節點發送數據命令了。客戶端向節點發送與數據庫鍵相關的命令時,若是鍵所在的槽正好就指派給了當前節點,那麼節點就直接執行命令;若是鍵所在的槽並無指派給當前節點,那麼節點返回一個MOVED錯誤,指引客戶端(redirect)至正確節點,並再次發送以前想要執行的命令。
1)計算鍵屬於哪一個槽 CLUSTER KEYSLOT [key]
CRC16(KEY) & 16383
2) 若計算的i不對應Myself 返回MOVED <slot> <ip>:<port>
3) 客戶端根據MOVED錯誤,轉向節點從新發送命令
5.從新分片
redis集羣的從新分片操做能夠將任意數量已經指派給某個節點的槽改成指派給另外一個節點,而且相關的槽所屬的鍵值對也會從源節點轉移到目標節點。能夠online下。
redis的從新分片操做時由redis的集羣管理軟件redis-trib負責執行的,redis提供了進從新分片所需的全部命令,而redis-trib則經過向源節點和目標節點發送命令來進行從新分片操做。步驟以下:
1)redis-trib對目標節點發送CLUSTER SETLOT < slot > IMPORTING < source_id> 準備好導入
2)redis-trib對源節點發送CLUSTER SETLOT < slot> MIGRATING < target_id > 準備好遷移
3)redis-trib對源節點發送CLUSTER GETKEYSINSLOT < slot > < count > 得到最多count個屬於槽slot的鍵值對的鍵名
4)對3中每一個鍵名,redis-trib對源節點發送MIGRATE < key_name> 0 < timeout> 遷移
5)重複3和4,知道槽中的鍵值對遷移到目標節點
6)redis-trib向任意節點發送CLUSTER SETLOT < slot> NODE < target_id>,將槽指派給目標節點,並經過消息告知整個集羣,最終全部節點都會知道槽slot已經指派給了目標節點。
6.ASK錯誤 處理正在遷移中槽錯誤
接到ASK錯誤的客戶端會根據錯誤提供的IP地址和端口號,轉向至正在導入槽的目標節點,而後向目標節點發送一個ASKING命令,再從新發送本來想要執行的命令。
ASKING命令加client.flags|=REDIS_ASKING。正常客戶端發送一個關於槽i的命令,而槽i又沒有指派給這個節點的話,會返回MOVED錯誤,設置了REDIS_ASKING後,則會破例執行
MOVED錯誤表明槽的負責權已經從一個節點轉移到另外一個,每次遇到都自動發到MOVED指向的節點。而ASK只是遷移槽中臨時的,下次對下次有影響
7.複製與故障轉移
1)複製
redis集羣中的節點分爲主節點和從節點,其中主節點用於處理槽,而從節點則用於複製主節點,並在被複制的主節點下線以後代替下線的主節點繼續處理命令請求。
設置從節點:CLUSTER REPLICATE < node_id> 讓接收命令的節點成爲node_id的從節點
接收到該命令的節點首先會在本身的clusterState.nodes字典裏面找到node_id對應的節點clusterNode結構,並將本身的clusterState.myself.slaveof指針指向這個結構;
節點會修改本身clusterState.myself.flags中的屬性,關閉原來的REDIS_NODE_MASTER標識,打開REDIS_NODE_SLAVE標識;
調用複製代碼,至關於向從節點發送SLAVEOF <master_ip> <master_port>。
2)故障檢測
集羣中的每一個節點都會按期地向集羣中的其餘節點發送PING消息,若是規定時間內沒有返回PONG,發送消息的節點就會把接受消息的節點標記爲疑似下線PFAIL。clusterNode的flags標識(REDIS_NODE_PFAIL)
集羣中各節點經過互相發送消息的方式交換集羣中各個節點的狀態信息,當A經過消息得知B認爲C進入疑似下線,A在本身clusterState.nodes中找到C對應的clusterNode結構將B的下線報告添加到該clusterNode的fail_reports中
半數以上主節點都報告x意思下線,則標記爲FAIL,將主節點x標記爲下線的節點向集羣廣播FAIL消息,全部接受者都將x標記爲FAIL
3)故障轉移
當一個從節點發現本身複製的主節點進入了下線狀態的時候,從節點將開始對下線主節點進行故障轉移,步驟以下:
選舉新的主節點 新的主節點執行SLAVEOF no one命令,成爲新的主節點 新的主節點將下線主節點的槽指派給本身 新的主節點向集羣廣播PONG消息,代表本身接管了原來下線節點的槽 新的節點開始接收和本身複製處理槽有關的命令請求。
選舉新的主節點
一樣基於Raft實現 從節點廣播CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST,未投過票的主節點返回CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK。配置紀元自增,半數以上。
8.消息
消息由消息頭(header)和消息正文(data)組成。
cluster.h/clusterMsg結構表示消息頭,cluster.h/clusterMsgData聯合體指向消息的正文。
節點消息分爲5類:
1)MEET
A接到客戶端發送的CLUSTER MEET B命令後,會向B發送MEET消息,B加入到A當前所處的集羣裏
2)PING
每一個節點默認1s/次從已知節點隨機選5個,對最長時間未發送PING的節點發送PING,或當有節點超過cluster-node-timeout的一半未收到PONG也發送PING,檢查節點是否在線
3)PONG
確認MEET,PING;或主動發送讓集羣中其餘節點當即刷新該節點信息,好比故障轉移操做成功後
以上三種消息都使用Gossip協議交換各自不一樣節點的信息,三種消息的正文都是由兩個cluster.h/clusterMsgDataGossip結構組成
發送者從本身已知節點列表中隨機選擇兩個節點(主、從),保存在兩個clusterMsgDataGossip結構中。接收者發現節點不在已知節點列表則與節點握手,不然更新信息。注意PONG也會帶兩個回去
4)FAIL
主節點判斷FAIL狀態,廣播
clusterMsgDataFail。(gossip隨機會慢)
5)PUBLISH
當節點收到一個PUBLISH,會執行這個命令並向集羣中廣播一條PUBLISH。即向集羣中某個節點發送PUBLISH <channel> <message>將致使集羣中全部節點都向channel頻道發送message消息。
要讓集羣全部節點都執行相同命令,能夠廣播,但還要用PUBLISH發是由於直接廣播這種作法,不符合redis集羣的「各個節點經過發送和接收消息來進行通訊」這一規則。
clusterMsgDataPublish
原生Gossip過程是由種子節點發起,當一個種子節點有狀態須要更新到網絡中的其餘節點時,它會隨機的選擇周圍幾個節點散播消息,收到消息的節點也會重複該過程,直至最終網絡中全部的節點都收到了消息。這個過程可能須要必定的時間,因爲不能保證某個時刻全部節點都收到消息,可是理論上最終全部節點都會收到消息,所以它是一個最終一致性協議。每次散播消息都選擇還沒有發送過的節點進行散播(有冗餘)
介紹codis的架構組件,可用性,一致性,擴展性
Codis 3.x 由如下組件組成:
1.使用自動負載均衡須要知足一個前提:全部codis-server的分組master必須配置maxmemory。
2.各組codis-server分配多少個slot是由其maxmemory決定。好比:A組maxmemory爲10G, B組maxmory爲1G,進行自動均衡處理後,A組分配的slot會是B組的10倍。
3.自動負載均衡並不會達到絕對意義上的均衡,其只作到maxmemory與分配的slot個數的比例均衡。沒法達到操做次數的均衡。
4.自動負載均衡的處理過程當中,若是發現存在maxmemory與分配的slot個數比例不均衡時,則會進行發起slot遷移的操做。達到均衡目的的前提下,此過程當中會作到儘可能減小slot的遷移。
codis和twemproxy最大的區別有兩個:
一個是codis支持動態水平擴展,對client徹底透明不影響服務的狀況下能夠完成增減redis實例的操做;
一個是codis是用go語言寫的並支持多線程而twemproxy用C並只用單線程。
後者又意味着:codis在多核機器上的性能會好於twemproxy;codis的最壞響應時間可能會由於GC的STW而變大,不過go1.5發佈後會顯著下降STW的時間;若是隻用一個CPU的話go語言的性能不如C,所以在一些短鏈接而非長鏈接的場景中,整個系統的瓶頸可能變成accept新tcp鏈接的速度,這時codis的性能可能會差於twemproxy。
數據量的限制。1024. 遷移比想象的頻繁 zk依賴,zk出問題,路由錯誤沒法發現,redis沒有路由信息
方案1:
方案2:
codis收到命令後發送給兩個機房的redis
方案3:
7.redis影響性能的命令:(執行時間長,傳輸數據多)
key*,sort(非要單獨機器
smembers 控制集合的數量,分子集,srandmember,
save bgsave/afo啓動時/master首次收到slave同步請求等時fork進程(fork時雖然數據寫時複製,但仍是會複製頁表,大頁能夠減小頁表,但改就會複製)
appendfsync everysec 子進程持久化和主進程 IO阻塞
bgrewriteaof AOF buffer和文件合併時阻塞的
問題:請求多,部分key集中於同一機器,沒法經過增長機器解決,源於redis的從都是備份恢復做用,codis等集羣也是
解決方案:
1.同時刻只有一個獲取鎖,2.多數節點可用則能夠獲取鎖,3.不會死鎖,4.鎖按期間有效1/2 :同時多個master上請求鎖,超過一半則成功3:master有時間自動釋放,監控每隔時間檢查釋放等4:釋放鎖須要密鑰,保證不釋放別人的鎖步驟:一、獲取當前時間(單位是毫秒)。二、輪流用相同的key和隨機值在N個節點上請求鎖,在這一步裏,客戶端在每一個master上請求鎖時,會有一個和總的鎖釋放時間相比小的多的超時時間。好比若是鎖自動釋放時間是10秒鐘,那每一個節點鎖請求的超時時間多是5-50毫秒的範圍,這個能夠防止一個客戶端在某個宕掉的master節點上阻塞過長時間,若是一個master節點不可用了,咱們應該儘快嘗試下一個master節點。三、客戶端計算第二步中獲取鎖所花的時間,只有當客戶端在大多數master節點上成功獲取了鎖(在這裏是3個),並且總共消耗的時間不超過鎖釋放時間,這個鎖就認爲是獲取成功了。四、若是鎖獲取成功了,那如今鎖自動釋放時間就是最初的鎖釋放時間減去以前獲取鎖所消耗的時間。五、若是鎖獲取失敗了,無論是由於獲取成功的鎖不超過一半(N/2+1)仍是由於總消耗時間超過了鎖釋放時間,客戶端都會到每一個master節點上釋放鎖,即使是那些他認爲沒有獲取成功的鎖。