溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

怎么用Redis實(shí)現(xiàn)分布式鎖

發(fā)布時(shí)間:2022-03-03 09:36:27 來(lái)源:億速云 閱讀:147 作者:iii 欄目:關(guān)系型數(shù)據(jù)庫(kù)

本文小編為大家詳細(xì)介紹“怎么用Redis實(shí)現(xiàn)分布式鎖”,內(nèi)容詳細(xì),步驟清晰,細(xì)節(jié)處理妥當(dāng),希望這篇“怎么用Redis實(shí)現(xiàn)分布式鎖”文章能幫助大家解決疑惑,下面跟著小編的思路慢慢深入,一起來(lái)學(xué)習(xí)新知識(shí)吧。

怎么用Redis實(shí)現(xiàn)分布式鎖

單機(jī)上的鎖和分布式鎖的聯(lián)系與區(qū)別

我們先來(lái)看下單機(jī)上的鎖。

對(duì)于在單機(jī)上運(yùn)行的多線程程序來(lái)說(shuō),鎖本身可以用一個(gè)變量表示。

  • 變量值為 0 時(shí),表示沒(méi)有線程獲取鎖;

  • 變量值為 1 時(shí),表示已經(jīng)有線程獲取到鎖了。

我們通常說(shuō)的線程調(diào)用加鎖和釋放鎖的操作,實(shí)際上,一個(gè)線程調(diào)用加鎖操作,其實(shí)就是檢查鎖變量值是否為 0。如果是 0,就把鎖的變量值設(shè)置為 1,表示獲取到鎖,如果不是 0,就返回錯(cuò)誤信息,表示加鎖失敗,已經(jīng)有別的線程獲取到鎖了。而一個(gè)線程調(diào)用釋放鎖操作,其實(shí)就是將鎖變量的值置為 0,以便其它線程可以來(lái)獲取鎖。
我用一段代碼來(lái)展示下加鎖和釋放鎖的操作,其中,lock 為鎖變量。

acquire_lock(){
  if lock == 0
     lock = 1
     return 1
  else
     return 0
} 
release_lock(){
  lock = 0
  return 1
}

和單機(jī)上的鎖類似,分布式鎖同樣可以用一個(gè)變量來(lái)實(shí)現(xiàn)??蛻舳思渔i和釋放鎖的操作邏輯,也和單機(jī)上的加鎖和釋放鎖操作邏輯一致:加鎖時(shí)同樣需要判斷鎖變量的值,根據(jù)鎖變量值來(lái)判斷能否加鎖成功;釋放鎖時(shí)需要把鎖變量值設(shè)置為 0,表明客戶端不再持有鎖。
但是,和線程在單機(jī)上操作鎖不同的是,在分布式場(chǎng)景下,鎖變量需要由一個(gè)共享存儲(chǔ)系統(tǒng)來(lái)維護(hù),只有這樣,多個(gè)客戶端才可以通過(guò)訪問(wèn)共享存儲(chǔ)系統(tǒng)來(lái)訪問(wèn)鎖變量。相應(yīng)的,加鎖和釋放鎖的操作就變成了讀取、判斷和設(shè)置共享存儲(chǔ)系統(tǒng)中的鎖變量值。

這樣一來(lái),我們就可以得出實(shí)現(xiàn)分布式鎖的兩個(gè)要求。

要求一:分布式鎖的加鎖和釋放鎖的過(guò)程,涉及多個(gè)操作。所以,在實(shí)現(xiàn)分布式鎖時(shí),我們需要保證這些鎖操作的原子性;
要求二:共享存儲(chǔ)系統(tǒng)保存了鎖變量,如果共享存儲(chǔ)系統(tǒng)發(fā)生故障或宕機(jī),那么客戶端也就無(wú)法進(jìn)行鎖操作了。在實(shí)現(xiàn)分布式鎖時(shí),我們需要考慮保證共享存儲(chǔ)系統(tǒng)的可靠性,進(jìn)而保證鎖的可靠性。

好了,知道了具體的要求,接下來(lái),我們就來(lái)學(xué)習(xí)下 Redis 是怎么實(shí)現(xiàn)分布式鎖的。

其實(shí),我們既可以基于單個(gè) Redis 節(jié)點(diǎn)來(lái)實(shí)現(xiàn),也可以使用多個(gè) Redis 節(jié)點(diǎn)實(shí)現(xiàn)。在這兩種情況下,鎖的可靠性是不一樣的。我們先來(lái)看基于單個(gè) Redis 節(jié)點(diǎn)的實(shí)現(xiàn)方法。

基于單個(gè) Redis 節(jié)點(diǎn)實(shí)現(xiàn)分布式鎖
作為分布式鎖實(shí)現(xiàn)過(guò)程中的共享存儲(chǔ)系統(tǒng),Redis 可以使用鍵值對(duì)來(lái)保存鎖變量,再接收和處理不同客戶端發(fā)送的加鎖和釋放鎖的操作請(qǐng)求。那么,鍵值對(duì)的鍵和值具體是怎么定的呢?
我們要賦予鎖變量一個(gè)變量名,把這個(gè)變量名作為鍵值對(duì)的鍵,而鎖變量的值,則是鍵值對(duì)的值,這樣一來(lái),Redis 就能保存鎖變量了,客戶端也就可以通過(guò) Redis 的命令操作來(lái)實(shí)現(xiàn)鎖操作。
為了幫助你理解,我畫了一張圖片,它展示 Redis 使用鍵值對(duì)保存鎖變量,以及兩個(gè)客戶端同時(shí)請(qǐng)求加鎖的操作過(guò)程。
怎么用Redis實(shí)現(xiàn)分布式鎖

可以看到,Redis 可以使用一個(gè)鍵值對(duì) lock_key:0 來(lái)保存鎖變量,其中,鍵是 lock_key,也是鎖變量的名稱,鎖變量的初始值是 0。

我們?cè)賮?lái)分析下加鎖操作。

在圖中,客戶端 A 和 C 同時(shí)請(qǐng)求加鎖。因?yàn)?Redis 使用單線程處理請(qǐng)求,所以,即使客戶端 A 和 C 同時(shí)把加鎖請(qǐng)求發(fā)給了 Redis,Redis 也會(huì)串行處理它們的請(qǐng)求。

我們假設(shè) Redis 先處理客戶端 A 的請(qǐng)求,讀取 lock_key 的值,發(fā)現(xiàn) lock_key 為 0,所以,Redis 就把 lock_key 的 value 置為 1,表示已經(jīng)加鎖了。緊接著,Redis 處理客戶端 C 的請(qǐng)求,此時(shí),Redis 會(huì)發(fā)現(xiàn) lock_key 的值已經(jīng)為 1 了,所以就返回加鎖失敗的信息。

剛剛說(shuō)的是加鎖的操作,那釋放鎖該怎么操作呢?其實(shí),釋放鎖就是直接把鎖變量值設(shè)置為 0。

我還是借助一張圖片來(lái)解釋一下。這張圖片展示了客戶端 A 請(qǐng)求釋放鎖的過(guò)程。當(dāng)客戶端 A 持有鎖時(shí),鎖變量 lock_key 的值為 1。客戶端 A 執(zhí)行釋放鎖操作后,Redis 將 lock_key 的值置為 0,表明已經(jīng)沒(méi)有客戶端持有鎖了。
怎么用Redis實(shí)現(xiàn)分布式鎖

因?yàn)榧渔i包含了三個(gè)操作(讀取鎖變量、判斷鎖變量值以及把鎖變量值設(shè)置為 1),而這三個(gè)操作在執(zhí)行時(shí)需要保證原子性。那怎么保證原子性呢?

要想保證操作的原子性,有兩種通用的方法,分別是使用 Redis 的單命令操作和使用 Lua 腳本。那么,在分布式加鎖場(chǎng)景下,該怎么應(yīng)用這兩個(gè)方法呢?

我們先來(lái)看下,Redis 可以用哪些單命令操作實(shí)現(xiàn)加鎖操作。

首先是 SETNX 命令,它用于設(shè)置鍵值對(duì)的值。具體來(lái)說(shuō),就是這個(gè)命令在執(zhí)行時(shí)會(huì)判斷鍵值對(duì)是否存在,如果不存在,就設(shè)置鍵值對(duì)的值,如果存在,就不做任何設(shè)置。

舉個(gè)例子,如果執(zhí)行下面的命令時(shí),key 不存在,那么 key 會(huì)被創(chuàng)建,并且值會(huì)被設(shè)置為 value;如果 key 已經(jīng)存在,SETNX 不做任何賦值操作。

SETNX key value

對(duì)于釋放鎖操作來(lái)說(shuō),我們可以在執(zhí)行完業(yè)務(wù)邏輯后,使用 DEL 命令刪除鎖變量。不過(guò),你不用擔(dān)心鎖變量被刪除后,其他客戶端無(wú)法請(qǐng)求加鎖了。因?yàn)?SETNX 命令在執(zhí)行時(shí),如果要設(shè)置的鍵值對(duì)(也就是鎖變量)不存在,SETNX 命令會(huì)先創(chuàng)建鍵值對(duì),然后設(shè)置它的值。所以,釋放鎖之后,再有客戶端請(qǐng)求加鎖時(shí),SETNX 命令會(huì)創(chuàng)建保存鎖變量的鍵值對(duì),并設(shè)置鎖變量的值,完成加鎖。
總結(jié)來(lái)說(shuō),我們就可以用 SETNX 和 DEL 命令組合來(lái)實(shí)現(xiàn)加鎖和釋放鎖操作。下面的偽代碼示例顯示了鎖操作的過(guò)程,你可以看下。

// 加鎖
SETNX lock_key 1
// 業(yè)務(wù)邏輯
DO THINGS
// 釋放鎖
DEL lock_key

不過(guò),使用 SETNX 和 DEL 命令組合實(shí)現(xiàn)分布鎖,存在兩個(gè)潛在的風(fēng)險(xiǎn)。

第一個(gè)風(fēng)險(xiǎn)是,假如某個(gè)客戶端在執(zhí)行了 SETNX 命令、加鎖之后,緊接著卻在操作共享數(shù)據(jù)時(shí)發(fā)生了異常,結(jié)果一直沒(méi)有執(zhí)行最后的 DEL 命令釋放鎖。因此,鎖就一直被這個(gè)客戶端持有,其它客戶端無(wú)法拿到鎖,也無(wú)法訪問(wèn)共享數(shù)據(jù)和執(zhí)行后續(xù)操作,這會(huì)給業(yè)務(wù)應(yīng)用帶來(lái)影響。
針對(duì)這個(gè)問(wèn)題,一個(gè)有效的解決方法是,給鎖變量設(shè)置一個(gè)過(guò)期時(shí)間。這樣一來(lái),即使持有鎖的客戶端發(fā)生了異常,無(wú)法主動(dòng)地釋放鎖,Redis 也會(huì)根據(jù)鎖變量的過(guò)期時(shí)間,在鎖變量過(guò)期后,把它刪除。其它客戶端在鎖變量過(guò)期后,就可以重新請(qǐng)求加鎖,這就不會(huì)出現(xiàn)無(wú)法加鎖的問(wèn)題了。

我們?cè)賮?lái)看第二個(gè)風(fēng)險(xiǎn)。如果客戶端 A 執(zhí)行了 SETNX 命令加鎖后,假設(shè)客戶端 B 執(zhí)行了 DEL 命令釋放鎖,此時(shí),客戶端 A 的鎖就被誤釋放了。如果客戶端 C 正好也在申請(qǐng)加鎖,就可以成功獲得鎖,進(jìn)而開(kāi)始操作共享數(shù)據(jù)。這樣一來(lái),客戶端 A 和 C 同時(shí)在對(duì)共享數(shù)據(jù)進(jìn)行操作,數(shù)據(jù)就會(huì)被修改錯(cuò)誤,這也是業(yè)務(wù)層不能接受的。
為了應(yīng)對(duì)這個(gè)問(wèn)題,我們需要能區(qū)分來(lái)自不同客戶端的鎖操作,具體咋做呢?其實(shí),我們可以在鎖變量的值上想想辦法。
在使用 SETNX 命令進(jìn)行加鎖的方法中,我們通過(guò)把鎖變量值設(shè)置為 1 或 0,表示是否加鎖成功。1 和 0 只有兩種狀態(tài),無(wú)法表示究竟是哪個(gè)客戶端進(jìn)行的鎖操作。所以,我們?cè)诩渔i操作時(shí),可以讓每個(gè)客戶端給鎖變量設(shè)置一個(gè)唯一值,這里的唯一值就可以用來(lái)標(biāo)識(shí)當(dāng)前操作的客戶端。在釋放鎖操作時(shí),客戶端需要判斷,當(dāng)前鎖變量的值是否和自己的唯一標(biāo)識(shí)相等,只有在相等的情況下,才能釋放鎖。這樣一來(lái),就不會(huì)出現(xiàn)誤釋放鎖的問(wèn)題了。

知道了解決方案,那么,在 Redis 中,具體是怎么實(shí)現(xiàn)的呢?我們?cè)賮?lái)了解下。
在查看具體的代碼前,我要先帶你學(xué)習(xí)下 Redis 的 SET 命令。

我們剛剛在說(shuō) SETNX 命令的時(shí)候提到,對(duì)于不存在的鍵值對(duì),它會(huì)先創(chuàng)建再設(shè)置值(也就是“不存在即設(shè)置”),為了能達(dá)到和 SETNX 命令一樣的效果,Redis 給 SET 命令提供了類似的選項(xiàng) NX,用來(lái)實(shí)現(xiàn)“不存在即設(shè)置”。如果使用了 NX 選項(xiàng),SET 命令只有在鍵值對(duì)不存在時(shí),才會(huì)進(jìn)行設(shè)置,否則不做賦值操作。此外,SET 命令在執(zhí)行時(shí)還可以帶上 EX 或 PX 選項(xiàng),用來(lái)設(shè)置鍵值對(duì)的過(guò)期時(shí)間。

舉個(gè)例子,執(zhí)行下面的命令時(shí),只有 key 不存在時(shí),SET 才會(huì)創(chuàng)建 key,并對(duì) key 進(jìn)行賦值。另外,key 的存活時(shí)間由 seconds 或者 milliseconds 選項(xiàng)值來(lái)決定。

SET key value [EX seconds | PX milliseconds]  [NX]

有了 SET 命令的 NX 和 EX/PX 選項(xiàng)后,我們就可以用下面的命令來(lái)實(shí)現(xiàn)加鎖操作了。
// 加鎖, unique_value作為客戶端唯一性的標(biāo)識(shí)

SET lock_key unique_value NX PX 10000

其中,unique_value 是客戶端的唯一標(biāo)識(shí),可以用一個(gè)隨機(jī)生成的字符串來(lái)表示,PX 10000 則表示 lock_key 會(huì)在 10s 后過(guò)期,以免客戶端在這期間發(fā)生異常而無(wú)法釋放鎖。

因?yàn)樵诩渔i操作中,每個(gè)客戶端都使用了一個(gè)唯一標(biāo)識(shí),所以在釋放鎖操作時(shí),我們需要判斷鎖變量的值,是否等于執(zhí)行釋放鎖操作的客戶端的唯一標(biāo)識(shí),如下所示:
//釋放鎖 比較unique_value是否相等,避免誤釋放

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

這是使用 Lua 腳本(unlock.script)實(shí)現(xiàn)的釋放鎖操作的偽代碼,其中,KEYS[1]表示 lock_key,ARGV[1]是當(dāng)前客戶端的唯一標(biāo)識(shí),這兩個(gè)值都是我們?cè)趫?zhí)行 Lua 腳本時(shí)作為參數(shù)傳入的。

最后,我們執(zhí)行下面的命令,就可以完成鎖釋放操作了。

redis-cli  --eval  unlock.script lock_key , unique_value

你可能也注意到了,在釋放鎖操作中,我們使用了 Lua 腳本,這是因?yàn)?,釋放鎖操作的邏輯也包含了讀取鎖變量、判斷值、刪除鎖變量的多個(gè)操作,而 Redis 在執(zhí)行 Lua 腳本時(shí),可以以原子性的方式執(zhí)行,從而保證了鎖釋放操作的原子性。

好了,到這里,你了解了如何使用 SET 命令和 Lua 腳本在 Redis 單節(jié)點(diǎn)上實(shí)現(xiàn)分布式鎖。但是,我們現(xiàn)在只用了一個(gè) Redis 實(shí)例來(lái)保存鎖變量,如果這個(gè) Redis 實(shí)例發(fā)生故障宕機(jī)了,那么鎖變量就沒(méi)有了。此時(shí),客戶端也無(wú)法進(jìn)行鎖操作了,這就會(huì)影響到業(yè)務(wù)的正常執(zhí)行。所以,我們?cè)趯?shí)現(xiàn)分布式鎖時(shí),還需要保證鎖的可靠性。那怎么提高呢?這就要提到基于多個(gè) Redis 節(jié)點(diǎn)實(shí)現(xiàn)分布式鎖的方式了。

基于多個(gè) Redis 節(jié)點(diǎn)實(shí)現(xiàn)高可靠的分布式鎖
當(dāng)我們要實(shí)現(xiàn)高可靠的分布式鎖時(shí),就不能只依賴單個(gè)的命令操作了,我們需要按照一定的步驟和規(guī)則進(jìn)行加解鎖操作,否則,就可能會(huì)出現(xiàn)鎖無(wú)法工作的情況?!耙欢ǖ牟襟E和規(guī)則”是指啥呢?其實(shí)就是分布式鎖的算法。

為了避免 Redis 實(shí)例故障而導(dǎo)致的鎖無(wú)法工作的問(wèn)題,Redis 的開(kāi)發(fā)者 Antirez 提出了分布式鎖算法 Redlock。

Redlock 算法的基本思路,是讓客戶端和多個(gè)獨(dú)立的 Redis 實(shí)例依次請(qǐng)求加鎖,如果客戶端能夠和半數(shù)以上的實(shí)例成功地完成加鎖操作,那么我們就認(rèn)為,客戶端成功地獲得分布式鎖了,否則加鎖失敗。這樣一來(lái),即使有單個(gè) Redis 實(shí)例發(fā)生故障,因?yàn)殒i變量在其它實(shí)例上也有保存,所以,客戶端仍然可以正常地進(jìn)行鎖操作,鎖變量并不會(huì)丟失。

我們來(lái)具體看下 Redlock 算法的執(zhí)行步驟。Redlock 算法的實(shí)現(xiàn)需要有 N 個(gè)獨(dú)立的 Redis 實(shí)例。接下來(lái),我們可以分成 3 步來(lái)完成加鎖操作。

第一步是,客戶端獲取當(dāng)前時(shí)間。
第二步是,客戶端按順序依次向 N 個(gè) Redis 實(shí)例執(zhí)行加鎖操作。

這里的加鎖操作和在單實(shí)例上執(zhí)行的加鎖操作一樣,使用 SET 命令,帶上 NX,EX/PX 選項(xiàng),以及帶上客戶端的唯一標(biāo)識(shí)。當(dāng)然,如果某個(gè) Redis 實(shí)例發(fā)生故障了,為了保證在這種情況下,Redlock 算法能夠繼續(xù)運(yùn)行,我們需要給加鎖操作設(shè)置一個(gè)超時(shí)時(shí)間。

如果客戶端在和一個(gè) Redis 實(shí)例請(qǐng)求加鎖時(shí),一直到超時(shí)都沒(méi)有成功,那么此時(shí),客戶端會(huì)和下一個(gè) Redis 實(shí)例繼續(xù)請(qǐng)求加鎖。加鎖操作的超時(shí)時(shí)間需要遠(yuǎn)遠(yuǎn)地小于鎖的有效時(shí)間,一般也就是設(shè)置為幾十毫秒。

第三步是,一旦客戶端完成了和所有 Redis 實(shí)例的加鎖操作,客戶端就要計(jì)算整個(gè)加鎖過(guò)程的總耗時(shí)。

客戶端只有在滿足下面的這兩個(gè)條件時(shí),才能認(rèn)為是加鎖成功。

  • 條件一:客戶端從超過(guò)半數(shù)(大于等于 N/2+1)的 Redis 實(shí)例上成功獲取到了鎖;

  • 條件二:客戶端獲取鎖的總耗時(shí)沒(méi)有超過(guò)鎖的有效時(shí)間。

在滿足了這兩個(gè)條件后,我們需要重新計(jì)算這把鎖的有效時(shí)間,計(jì)算的結(jié)果是鎖的最初有效時(shí)間減去客戶端為獲取鎖的總耗時(shí)。如果鎖的有效時(shí)間已經(jīng)來(lái)不及完成共享數(shù)據(jù)的操作了,我們可以釋放鎖,以免出現(xiàn)還沒(méi)完成數(shù)據(jù)操作,鎖就過(guò)期了的情況。

當(dāng)然,如果客戶端在和所有實(shí)例執(zhí)行完加鎖操作后,沒(méi)能同時(shí)滿足這兩個(gè)條件,那么,客戶端向所有 Redis 節(jié)點(diǎn)發(fā)起釋放鎖的操作。

在 Redlock 算法中,釋放鎖的操作和在單實(shí)例上釋放鎖的操作一樣,只要執(zhí)行釋放鎖的 Lua 腳本就可以了。這樣一來(lái),只要 N 個(gè) Redis 實(shí)例中的半數(shù)以上實(shí)例能正常工作,就能保證分布式鎖的正常工作了。

所以,在實(shí)際的業(yè)務(wù)應(yīng)用中,如果你想要提升分布式鎖的可靠性,就可以通過(guò) Redlock 算法來(lái)實(shí)現(xiàn)。

讀到這里,這篇“怎么用Redis實(shí)現(xiàn)分布式鎖”文章已經(jīng)介紹完畢,想要掌握這篇文章的知識(shí)點(diǎn)還需要大家自己動(dòng)手實(shí)踐使用過(guò)才能領(lǐng)會(huì),如果想了解更多相關(guān)內(nèi)容的文章,歡迎關(guān)注億速云行業(yè)資訊頻道。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI