摘要:在前文中提及了實現(xiàn)分布式鎖目前有三種流行方案,分別為基于數(shù)據(jù)庫、Redis、Zookeeper的方案,本文主要闡述基于Redis的分布式鎖,分布式架構設計如今在企業(yè)中被大量的應用,而在不同的分布式節(jié)點進行協(xié)同工作的時候,節(jié)點服務的時序、結果的正確性以及執(zhí)行成本也成為了必須考慮的重要因素。其中競態(tài)條件會導致執(zhí)行結果的不正確,不同服務節(jié)點同時處理同一任務也將耗費不必需的系統(tǒng)資源,如果解決呢?方式之一可以選擇分布式鎖,本文介紹如果通過redis實現(xiàn)分布式鎖,也歡迎大家和我一起討論。
分布式鎖的基本應用場景和設計原則
我們先來看一個簡單的案例:有三個服務,一個是訂單服務orderService,一個是報表服務(reportService),一個是推送服務(pushService),每個服務都橫向部署在2個節(jié)點上。報表服務每天凌晨12點需要從訂單服務拉取訂單數(shù)據(jù)并生成報表,并且在每天早上8點通過推送服務向用戶發(fā)送新生成的數(shù)據(jù)報表,需要如何設計這個流程?
首先我們需要了解該流程的兩個關鍵點,第一,報表服務的2個節(jié)點只能有一個節(jié)點生成報表,否則會浪費系統(tǒng)資源,該關鍵點沒有高可靠的要求(重復覆蓋生成并不會得到錯誤結果);第二,向同一個用戶推送該數(shù)據(jù)報表也只能有一個節(jié)點去執(zhí)行,否則用戶會收到兩份一樣的報表,該關鍵點有高可靠要求。
我們可以從兩個關鍵點中提取一個相同點,必須要設置一把鎖,獲的該鎖的節(jié)點才能執(zhí)行指定的任務。同時還能提取到一個不同點,那就是兩種場景對獲取鎖的依賴程度不一致。我們來對該流程進行簡單建模:
通過上圖的流程已經(jīng)可以實現(xiàn)簡單可靠的鎖機制,當然這是有前提的。
首先鎖服務必須足夠穩(wěn)定,假設無法獲取鎖,那么競爭任務的將無法執(zhí)行。其次,執(zhí)行競爭任務的過程不能夠死鎖或者無限等待,否則將無法釋放鎖且改任務也無法執(zhí)行完成。所以在設計鎖的時候還需要考慮兩個因素:鎖必須要有過期時間及獲取及釋放鎖過程的高可用或者鎖錯誤時的異常處理。
所以,歸納一下分布式鎖在設計時通常要考慮的幾個要素是:
分布式鎖一定要保證多客戶端競爭臨界資源時的絕對互斥;
分布式鎖要設計一定的超時時間,防止在獲得鎖的服務阻塞或者崩潰引起的鎖無法釋放;
分布式要針對業(yè)務場景設計鎖機制異常降級措施,防止因為鎖獲取錯誤導致無法獲取臨界資源的后果。
關于第2點的要素,還有一些要注意的東西,假設報表服務A在獲取到鎖之后,出現(xiàn)了很長的FULL GC,系統(tǒng)出現(xiàn)暫停,在此期間,鎖已經(jīng)超時了,報表服務B又重新拿到了鎖并向用戶發(fā)送了報表,在客戶端AFull GC結束后,同樣再去執(zhí)行報表發(fā)送任務,就會導致執(zhí)行結果出錯。
這種場景往往需要個性化的處理,現(xiàn)在業(yè)界大部分的分布式鎖都會出現(xiàn)這種情況,因為系統(tǒng)暫停導致的鎖失效往往很難去避免,因為系統(tǒng)暫停可能出現(xiàn)在任何時候。 通常情況下,我們需要預估訪問競爭資源的時間,確定好超時時間并在訪問結束后進行數(shù)據(jù)比對和必要的數(shù)據(jù)補償。
Redis具體實現(xiàn)分布式鎖
在redis命令集合中,有一個命令叫做SETNX,具體命令格式是:SETNX key value
該命令的作用是如果key存在,則什么都不做,并且返回0,如果key不存在則將key的值設置成value,并且返回1,該命令是原子性的。我們可以利用該命令來實現(xiàn)分布式鎖。
獲取鎖:獲取當前的timestamp,并將客戶端ID作為key,該timestamp作為value調用SETNX,并設置鎖的TTL,處理獲取鎖的異常。
確認鎖狀態(tài),如果成功獲取鎖,則訪問臨界資源,否則根據(jù)業(yè)務場景間隔一定時間再次嘗試獲取鎖。
訪問臨界資源
釋放鎖
//獲取鎖
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é)點,當其因為不可控原因宕機時,鎖機制將不可用。有人可能會說,可以使用redis主從集群復制,主掛了,從可以接替上,但是這估計依然不能解決問題,因為redis主從復制是異步的,誰能保證主掛了,從節(jié)點上一定有鎖數(shù)據(jù)呢?
redis官網(wǎng)上介紹了一種red lock算法,該算法棄用了單redis節(jié)點,采用N個(官網(wǎng)推薦5個)獨立的redis節(jié)點作為鎖服務,客戶端要獲取鎖,必須向N/2+1(絕大部分)節(jié)點成功申請鎖后,才能訪問臨界資源。
但是該算法中獲取鎖的過程變的復雜了,時間也就越不可控,假設從redis1節(jié)點獲取鎖成功開始到從redis(N/2+1)獲取鎖成功結束到時間為SPACETIME,鎖到有效時間不再是key到TTL,而是:
REMAIN_TIME=TTL-SPACETIME
當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è)界非常常用,但是我們在應用過程中一定要注意實現(xiàn)鎖到超時避免死鎖以及因為服務暫停導致鎖失效到情況,每種情況到解決方案需要個性化到去解決。Red lock算法在一定程度上解決了分布式鎖服務的穩(wěn)定性問題,但是帶來了系統(tǒng)復雜度,同時也有人在質疑了該算法,有興趣到可以在搜索引擎搜索。本文就到這里,如有錯誤,歡迎指正。
想要了解更多分布式知識點的,可以加群:?537775426(備注好信息),我會把關于分布式的知識點放在群的共享區(qū)里面,我也會在群里面分享我從業(yè)多年的一些工作經(jīng)驗,希望我的工作經(jīng)驗可以幫助大家在成為架構師的道路上面少走彎路。帶著大家全面、科學地建立自己的技術體系和技術認知!