摘要:在前文中提及了實現(xiàn)分布式鎖目前有三種流行方案州袒,分別為基于數(shù)據(jù)庫、Redis弓候、Zookeeper的方案郎哭,本文主要闡述基于Redis的分布式鎖,分布式架構(gòu)設(shè)計如今在企業(yè)中被大量的應(yīng)用菇存,而在不同的分布式節(jié)點進行協(xié)同工作的時候夸研,節(jié)點服務(wù)的時序、結(jié)果的正確性以及執(zhí)行成本也成為了必須考慮的重要因素依鸥。其中競態(tài)條件會導(dǎo)致執(zhí)行結(jié)果的不正確亥至,不同服務(wù)節(jié)點同時處理同一任務(wù)也將耗費不必需的系統(tǒng)資源,如果解決呢贱迟?方式之一可以選擇分布式鎖姐扮,本文介紹如果通過redis實現(xiàn)分布式鎖,也歡迎大家和我一起討論衣吠。
分布式鎖的基本應(yīng)用場景和設(shè)計原則
我們先來看一個簡單的案例:有三個服務(wù)茶敏,一個是訂單服務(wù)orderService,一個是報表服務(wù)(reportService)缚俏,一個是推送服務(wù)(pushService)惊搏,每個服務(wù)都橫向部署在2個節(jié)點上。報表服務(wù)每天凌晨12點需要從訂單服務(wù)拉取訂單數(shù)據(jù)并生成報表忧换,并且在每天早上8點通過推送服務(wù)向用戶發(fā)送新生成的數(shù)據(jù)報表恬惯,需要如何設(shè)計這個流程?
首先我們需要了解該流程的兩個關(guān)鍵點亚茬,第一酪耳,報表服務(wù)的2個節(jié)點只能有一個節(jié)點生成報表,否則會浪費系統(tǒng)資源刹缝,該關(guān)鍵點沒有高可靠的要求(重復(fù)覆蓋生成并不會得到錯誤結(jié)果)葡兑;第二,向同一個用戶推送該數(shù)據(jù)報表也只能有一個節(jié)點去執(zhí)行赞草,否則用戶會收到兩份一樣的報表讹堤,該關(guān)鍵點有高可靠要求。
我們可以從兩個關(guān)鍵點中提取一個相同點厨疙,必須要設(shè)置一把鎖洲守,獲的該鎖的節(jié)點才能執(zhí)行指定的任務(wù)。同時還能提取到一個不同點,那就是兩種場景對獲取鎖的依賴程度不一致梗醇。我們來對該流程進行簡單建模:
通過上圖的流程已經(jīng)可以實現(xiàn)簡單可靠的鎖機制知允,當(dāng)然這是有前提的。
首先鎖服務(wù)必須足夠穩(wěn)定叙谨,假設(shè)無法獲取鎖温鸽,那么競爭任務(wù)的將無法執(zhí)行。其次手负,執(zhí)行競爭任務(wù)的過程不能夠死鎖或者無限等待涤垫,否則將無法釋放鎖且改任務(wù)也無法執(zhí)行完成。所以在設(shè)計鎖的時候還需要考慮兩個因素:鎖必須要有過期時間及獲取及釋放鎖過程的高可用或者鎖錯誤時的異常處理竟终。
所以蝠猬,歸納一下分布式鎖在設(shè)計時通常要考慮的幾個要素是:
分布式鎖一定要保證多客戶端競爭臨界資源時的絕對互斥;
分布式鎖要設(shè)計一定的超時時間统捶,防止在獲得鎖的服務(wù)阻塞或者崩潰引起的鎖無法釋放榆芦;
分布式要針對業(yè)務(wù)場景設(shè)計鎖機制異常降級措施,防止因為鎖獲取錯誤導(dǎo)致無法獲取臨界資源的后果喘鸟。
關(guān)于第2點的要素匆绣,還有一些要注意的東西,假設(shè)報表服務(wù)A在獲取到鎖之后什黑,出現(xiàn)了很長的FULL GC崎淳,系統(tǒng)出現(xiàn)暫停,在此期間兑凿,鎖已經(jīng)超時了凯力,報表服務(wù)B又重新拿到了鎖并向用戶發(fā)送了報表茵瘾,在客戶端AFull GC結(jié)束后礼华,同樣再去執(zhí)行報表發(fā)送任務(wù),就會導(dǎo)致執(zhí)行結(jié)果出錯拗秘。
這種場景往往需要個性化的處理圣絮,現(xiàn)在業(yè)界大部分的分布式鎖都會出現(xiàn)這種情況,因為系統(tǒng)暫停導(dǎo)致的鎖失效往往很難去避免雕旨,因為系統(tǒng)暫桶缃常可能出現(xiàn)在任何時候。 通常情況下凡涩,我們需要預(yù)估訪問競爭資源的時間棒搜,確定好超時時間并在訪問結(jié)束后進行數(shù)據(jù)比對和必要的數(shù)據(jù)補償。
Redis具體實現(xiàn)分布式鎖
在redis命令集合中活箕,有一個命令叫做SETNX力麸,具體命令格式是:SETNX key value
該命令的作用是如果key存在,則什么都不做,并且返回0克蚂,如果key不存在則將key的值設(shè)置成value闺鲸,并且返回1,該命令是原子性的埃叭。我們可以利用該命令來實現(xiàn)分布式鎖摸恍。
獲取鎖:獲取當(dāng)前的timestamp,并將客戶端ID作為key赤屋,該timestamp作為value調(diào)用SETNX立镶,并設(shè)置鎖的TTL,處理獲取鎖的異常益缎。
確認(rèn)鎖狀態(tài)谜慌,如果成功獲取鎖,則訪問臨界資源莺奔,否則根據(jù)業(yè)務(wù)場景間隔一定時間再次嘗試獲取鎖欣范。
訪問臨界資源
釋放鎖
//獲取鎖
timeStamp?=?getCurrentTimeStamp();
try{
? ?lock=SET CLIENT_ID timeStamp NX PX TIMEOUT;
}catch(Exception?e){
? ?//處理獲取鎖的異常
? ?return;
}
try{
? ?if(lock?==?0){
? ? ? ?return;
? ?}else{
? ? ? ?//訪問臨界資源
? ? ? ?do();
? ?}
}finally{
? ?//釋放鎖
? ?del?CLIENT_ID;
}
這種實現(xiàn)分布式鎖的方式是很多開發(fā)者最喜歡用的,但是如何保證redis的可用性呢令哟,如果我們使用一個redis節(jié)點恼琼,當(dāng)其因為不可控原因宕機時,鎖機制將不可用屏富。有人可能會說晴竞,可以使用redis主從集群復(fù)制,主掛了狠半,從可以接替上噩死,但是這估計依然不能解決問題,因為redis主從復(fù)制是異步的神年,誰能保證主掛了窍奋,從節(jié)點上一定有鎖數(shù)據(jù)呢考榨?
redis官網(wǎng)上介紹了一種red lock算法悼泌,該算法棄用了單redis節(jié)點瓶珊,采用N個(官網(wǎng)推薦5個)獨立的redis節(jié)點作為鎖服務(wù),客戶端要獲取鎖飘千,必須向N/2+1(絕大部分)節(jié)點成功申請鎖后堂鲜,才能訪問臨界資源。
但是該算法中獲取鎖的過程變的復(fù)雜了护奈,時間也就越不可控缔莲,假設(shè)從redis1節(jié)點獲取鎖成功開始到從redis(N/2+1)獲取鎖成功結(jié)束到時間為SPACETIME,鎖到有效時間不再是key到TTL霉旗,而是:
REMAIN_TIME=TTL-SPACETIME
當(dāng)SPACETIME比較大時痴奏,客戶端非常有可能獲取到一個已經(jīng)失效到鎖磺箕,所以在獲取鎖之后red lock算法需要再次驗證鎖是否失效。
//獲取鎖
timeStamp?=?getCurrentTimeStamp();
//向N/2+1個節(jié)點申請鎖
int?successLockNum=0;
boolean?lockSuccess=false;
for(int?i=1;i<5;i++){
? ?try{
? ? ? ?lock=SET CLIENT_ID timeStamp NX PX TIMEOUT;
? ? ? ?if(lock?==?1?&&?++successLockNum?==?N/2+1){
lockSuccess?=?true;
? ? ? ? ? ?break;
? ? ? ?}
? ?}catch(Exception?e){
? ? ? ?//處理獲取鎖的異常
? ? ? ?return;
? ?}
}
//驗證獲取鎖是否成功
if(!successLockNum){
? ?//獲取鎖失敗
? ?return;
}
//驗證獲取到到鎖是否是無效鎖
nowTimeStamp?=?getCurrentTimeStamp();
if(nowTimeStamp-timeStamp>TTL){
? ?//無效鎖
? ?return;
}
try{
? ?//訪問臨界資源
? ?do();
}finally{
? ?//釋放鎖
? ?del?CLIENT_ID;
}
后續(xù)
用Redis來實現(xiàn)分布式鎖機制在業(yè)界非常常用抛虫,但是我們在應(yīng)用過程中一定要注意實現(xiàn)鎖到超時避免死鎖以及因為服務(wù)暫停導(dǎo)致鎖失效到情況松靡,每種情況到解決方案需要個性化到去解決。Red lock算法在一定程度上解決了分布式鎖服務(wù)的穩(wěn)定性問題建椰,但是帶來了系統(tǒng)復(fù)雜度雕欺,同時也有人在質(zhì)疑了該算法,有興趣到可以在搜索引擎搜索棉姐。本文就到這里屠列,如有錯誤,歡迎指正伞矩。
想要了解更多分布式知識點的笛洛,可以加群:?537775426(備注好信息),我會把關(guān)于分布式的知識點放在群的共享區(qū)里面乃坤,我也會在群里面分享我從業(yè)多年的一些工作經(jīng)驗苛让,希望我的工作經(jīng)驗可以幫助大家在成為架構(gòu)師的道路上面少走彎路。帶著大家全面湿诊、科學(xué)地建立自己的技術(shù)體系和技術(shù)認(rèn)知狱杰!