天天看點

【面試題】如何用Redis實作分布式鎖

分布式鎖其實就是,控制分布式系統不同程序共同通路共享資源的一種鎖的實作。如果不同的系統或同一個系統的不同主機之間共享了某個臨界資源,往往需要互斥來防止彼此幹擾,以保證一緻性。

「互斥性」: 任意時刻,隻有一個用戶端能持有鎖。

「鎖逾時釋放」:持有鎖逾時,可以釋放,防止不必要的資源浪費,也可以防止死鎖。

「可重入性」:一個線程如果擷取了鎖之後,可以再次對其請求加鎖。

「高性能和高可用」:加鎖和解鎖需要開銷盡可能低,同時也要保證高可用,避免分布式鎖失效。

「安全性」:鎖隻能被持有的用戶端删除,不能被其他用戶端删除

即先用<code>setnx</code>來搶鎖,如果搶到之後,再用<code>expire</code>給鎖設定一個過期時間,防止鎖忘記了釋放。

但是這個方案中,<code>setnx</code>和<code>expire</code>兩個指令分開了,「不是原子操作」。如果執行完<code>setnx</code>加鎖,正要執行<code>expire</code>設定過期時間時,程序crash或者要重新開機維護了,那麼這個鎖就成為死鎖了,「别的線程永遠擷取不到鎖」

為了解決方案一,「發生異常鎖得不到釋放的場景」,可以把過期時間放到<code>setnx</code>的value值裡面。如果加鎖失敗,再拿出value值校驗一下即可。加鎖代碼如下:

這個方案的優點是,巧妙移除<code>expire</code>單獨設定過期時間的操作,把「過期時間放到setnx的value值」裡面來。解決了方案一發生異常,鎖得不到釋放的問題。但是這個方案還有别的缺點:

過期時間是用戶端自己生成的(System.currentTimeMillis()是目前系統的時間),必須要求分布式環境下,每個用戶端的時間必須同步。

如果鎖過期的時候,并發多個用戶端同時請求過來,都執行jedis.getSet(),最終隻能有一個用戶端加鎖成功,但是該用戶端鎖的過期時間,可能被别的用戶端覆寫

該鎖沒有儲存持有者的唯一辨別,可能被别的用戶端釋放/解鎖。

注:

在現在叢集、微服務多機部署運作環境中,都需要做時間同步,尤其有消息隊列環境。 該鎖沒有唯一辨別的另一種解法是,valueStr=唯一辨別+分隔符+expiresStr。【不優雅】

實際上,我們還可以使用Lua腳本來保證原子性(包含setnx和expire兩條指令),lua腳本如下:

加鎖代碼如下:

除了使用,使用Lua腳本,保證<code>SETNX + EXPIRE</code>兩條指令的原子性,我們還可以巧用Redis的SET指令擴充參數!(<code>SET key value[EX seconds][PX milliseconds][NX|XX]</code>),它也是原子性的!

SET key value[EX seconds][PX milliseconds][NX|XX] NX :表示key不存在的時候,才能set成功,也即保證隻有第一個用戶端請求才能獲得鎖,而其他用戶端請求隻能等其釋放鎖,才能擷取。 EX seconds :設定key的過期時間,時間機關是秒。 PX milliseconds: 設定key的過期時間,機關為毫秒 XX: 僅當key存在時設定值

僞代碼demo如下:

但是呢,這個方案還是可能存在問題:

問題一:「鎖過期釋放了,業務還沒執行完」。假設線程a擷取鎖成功,一直在執行臨界區的代碼。但是100s過去後,它還沒執行完。但是,這時候鎖已經過期了,此時線程b又請求過來。顯然線程b就可以獲得鎖成功,也開始執行臨界區的代碼。那麼問題就來了,臨界區的業務代碼都不是嚴格串行執行的啦。

問題二:「鎖被别的線程誤删」。假設線程a執行完後,去釋放鎖。但是它不知道目前的鎖可能是線程b持有的(線程a去釋放鎖時,有可能過期時間已經到了,此時線程b進來占有了鎖)。那線程a就把線程b的鎖釋放掉了,但是線程b臨界區業務代碼可能都還沒執行完呢。

既然鎖可能被别的線程誤删,那我們給value值設定一個标記目前線程唯一的随機數,在删除的時候,校驗一下,不就OK了嘛。僞代碼如下:

在這裡,「判斷是不是目前線程加的鎖」和「釋放鎖」不是一個原子操作。如果調用jedis.del()釋放鎖的時候,可能這把鎖已經不屬于目前用戶端,會解除他人加的鎖。

【面試題】如何用Redis實作分布式鎖

為了更嚴謹,一般也是用lua腳本代替。lua腳本如下:

方案五還是可能存在「鎖過期釋放,業務沒執行完」的問題。有些小夥伴認為,稍微把鎖過期時間設定長一些就可以啦。其實我們設想一下,是否可以給獲得鎖的線程,開啟一個定時守護線程,每隔一段時間檢查鎖是否還存在,存在則對鎖的過期時間延長,防止鎖過期提前釋放。

目前開源架構Redisson解決了這個問題。我們一起來看下Redisson底層原理圖吧:

【面試題】如何用Redis實作分布式鎖

隻要線程一加鎖成功,就會啟動一個<code>watch dog</code>看門狗,它是一個背景線程,會每隔10秒檢查一下,如果線程1還持有鎖,那麼就會不斷的延長鎖key的生存時間。是以,Redisson就是使用watch dog解決了「鎖過期釋放,業務沒執行完」問題。

前面六種方案都隻是基于單機版的讨論,還不是很完美。其實Redis一般都是叢集部署的:

【面試題】如何用Redis實作分布式鎖

如果線程一在Redis的master節點上拿到了鎖,但是加鎖的key還沒同步到slave節點。恰好這時,master節點發生故障,一個slave節點就會更新為master節點。線程二就可以擷取同個key的鎖啦,但線程一也已經拿到鎖了,鎖的安全性就沒了。

為了解決這個問題,Redis作者 antirez提出一種進階的分布式鎖算法:Redlock。Redlock核心思想是這樣的:

搞多個Redis master部署,以保證它們不會同時宕掉。并且這些master節點是完全互相獨立的,互相之間不存在資料同步。同時,需要確定在這多個master執行個體上,是與在Redis單執行個體,使用相同方法來擷取和釋放鎖。

我們假設目前有5個Redis master節點,在5台伺服器上面運作這些Redis執行個體。

【面試題】如何用Redis實作分布式鎖

RedLock的實作步驟:如下

1.擷取目前時間,以毫秒為機關。

2.按順序向5個master節點請求加鎖。用戶端設定網絡連接配接和響應逾時時間,并且逾時時間要小于鎖的失效時間。(假設鎖自動失效時間為10秒,則逾時時間一般在5-50毫秒之間,我們就假設逾時時間是50ms吧)。如果逾時,跳過該master節點,盡快去嘗試下一個master節點。

3.用戶端使用目前時間減去開始擷取鎖時間(即步驟1記錄的時間),得到擷取鎖使用的時間。當且僅當超過一半(N/2+1,這裡是5/2+1=3個節點)的Redis master節點都獲得鎖,并且使用的時間小于鎖失效時間時,鎖才算擷取成功。(如上圖,10s&gt; 30ms+40ms+50ms+4m0s+50ms)

如果取到了鎖,key的真正有效時間就變啦,需要減去擷取鎖所使用的時間。

如果擷取鎖失敗(沒有在至少N/2+1個master執行個體取到鎖,有或者擷取鎖時間已經超過了有效時間),用戶端要在所有的master節點上解鎖(即便有些master節點根本就沒有加鎖成功,也需要解鎖,以防止有些漏網之魚)。

簡化下步驟就是:

按順序向5個master節點請求加鎖

根據設定的逾時時間來判斷,是不是要跳過該master節點。

如果大于等于3個節點加鎖成功,并且使用的時間小于鎖的有效期,即可認定加鎖成功啦。

如果擷取鎖失敗,解鎖!

轉載:七種方案!探讨Redis分布式鎖的正确使用姿勢 - 文章詳情 (itpub.net)

好學若饑,謙卑若愚