溫馨提示×

溫馨提示×

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

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

如何用Redis分布式鎖才能確保萬無一失

發(fā)布時間:2021-10-22 09:51:04 來源:億速云 閱讀:102 作者:iii 欄目:數(shù)據(jù)庫

本篇內(nèi)容介紹了“如何用Redis分布式鎖才能確保萬無一失”的有關(guān)知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!

一、背景

我們?nèi)粘T陔娚叹W(wǎng)站購物時經(jīng)常會遇到一些高并發(fā)的場景,例如電商 App  上經(jīng)常出現(xiàn)的秒殺活動、限量優(yōu)惠券搶購,還有我們?nèi)ツ膬壕W(wǎng)的火車票搶票系統(tǒng)等,這些場景有一個共同特點就是訪問量激增,雖然在系統(tǒng)設(shè)計時會通過限流、異步、排隊等方式優(yōu)化,但整體的并發(fā)還是平時的數(shù)倍以上,為了避免并發(fā)問題,防止庫存超賣,給用戶提供一個良好的購物體驗,這些系統(tǒng)中都會用到鎖的機(jī)制。

對于單進(jìn)程的并發(fā)場景,可以使用編程語言及相應(yīng)的類庫提供的鎖,如 Java 中的 synchronized 語法以及 ReentrantLock 類等,避免并發(fā)問題。

如何用Redis分布式鎖才能確保萬無一失

如果在分布式場景中,實現(xiàn)不同客戶端的線程對代碼和資源的同步訪問,保證在多線程下處理共享數(shù)據(jù)的安全性,就需要用到分布式鎖技術(shù)。

如何用Redis分布式鎖才能確保萬無一失

那么何為分布式鎖呢?分布式鎖是控制分布式系統(tǒng)或不同系統(tǒng)之間共同訪問共享資源的一種鎖實現(xiàn),如果不同的系統(tǒng)或同一個系統(tǒng)的不同主機(jī)之間共享了某個資源時,往往需要互斥來防止彼此干擾保證一致性。

一個相對安全的分布式鎖,一般需要具備以下特征:

  • 互斥性?;コ馐擎i的基本特征,同一時刻鎖只能被一個線程持有,執(zhí)行臨界區(qū)操作。

  • 超時釋放。通過超時釋放,可以避免死鎖,防止不必要的線程等待和資源浪費,類似于 MySQL 的 InnoDB 引擎中的 innodblockwait_timeout 參數(shù)配置。

  • 可重入性。一個線程在持有鎖的情況可以對其再次請求加鎖,防止鎖在線程執(zhí)行完臨界區(qū)操作之前釋放。

  • 高性能和高可用。加鎖和釋放鎖的過程性能開銷要盡可能的低,同時也要保證高可用,防止分布式鎖意外失效。

可以看出實現(xiàn)分布式鎖,并不是鎖住資源就可以了,還需要滿足一些額外的特征,避免出現(xiàn)死鎖、鎖失效等問題。

二、分布式鎖的實現(xiàn)方式

目前實現(xiàn)分布式鎖的方式有很多,常見的主要有:

  • Memcached 分布式鎖

利用 Memcached 的 add 命令。此命令是原子性操作,只有在 key 不存在的情況下,才能 add 成功,也就意味著線程得到了鎖。

  • Zookeeper 分布式鎖

利用 Zookeeper 的順序臨時節(jié)點,來實現(xiàn)分布式鎖和等待隊列。ZooKeeper  作為一個專門為分布式應(yīng)用提供方案的框架,它提供了一些非常好的特性,如 ephemeral 類型的 znode 自動刪除的功能,同時  ZooKeeper 還提供 watch 機(jī)制,可以讓分布式鎖在客戶端用起來就像一個本地的鎖一樣:加鎖失敗就阻塞住,直到獲取到鎖為止。

  • Chubby

Google 公司實現(xiàn)的粗粒度分布式鎖服務(wù),有點類似于 ZooKeeper,但也存在很多差異。Chubby 通過 sequencer 機(jī)制解決了請求延遲造成的鎖失效的問題。

  • Redis 分布式鎖

基于 Redis 單機(jī)實現(xiàn)的分布式鎖,其方式和 Memcached 的實現(xiàn)方式類似,利用 Redis 的 SETNX  命令,此命令同樣是原子性操作,只有在 key 不存在的情況下,才能 set 成功。而基于 Redis 多機(jī)實現(xiàn)的分布式鎖Redlock,是  Redis 的作者 antirez 為了規(guī)范 Redis 分布式鎖的實現(xiàn),提出的一個更安全有效的實現(xiàn)機(jī)制。

本文主要討論分析基于Redis的分布式鎖的幾種實現(xiàn)方式以及存在的問題。

三、Redis分布式鎖

使用 Redis 作為分布式鎖,本質(zhì)上要實現(xiàn)的目標(biāo)就是一個進(jìn)程在 Redis 里面占據(jù)了僅有的一個“茅坑”,當(dāng)別的進(jìn)程也想來占坑時,發(fā)現(xiàn)已經(jīng)有人蹲在那里了,就只好放棄或者等待稍后再試。

目前基于 Redis 實現(xiàn)分布式鎖主要有兩大類,一類是基于單機(jī),另一類是基于 Redis 多機(jī),不管是哪種實現(xiàn)方式,均需要實現(xiàn)加鎖、解鎖、鎖超時這三個分布式鎖的核心要素。

1、基于Redis單機(jī)實現(xiàn)的分布式鎖

1)使用 SETNX 指令

最簡單的加鎖方式就是直接使用 Redis 的 SETNX 指令,該指令只在 key 不存在的情況下,將 key 的值設(shè)置為 value,若 key 已經(jīng)存在,則 SETNX 命令不做任何動作。key 是鎖的唯一標(biāo)識,可以按照業(yè)務(wù)需要鎖定的資源來命名。

比如在某商城的秒殺活動中對某一商品加鎖,那么 key 可以設(shè)置為 lock_resource_id ,value 可以設(shè)置為任意值,在資源使用完成后,使用 DEL 刪除該 key 對鎖進(jìn)行釋放,整個過程如下:

如何用Redis分布式鎖才能確保萬無一失

很顯然,這種獲取鎖的方式很簡單,但也存在一個問題,就是我們上面提到的分布式鎖三個核心要素之一的鎖超時問題,即如果獲得鎖的進(jìn)程在業(yè)務(wù)邏輯處理過程中出現(xiàn)了異常,可能會導(dǎo)致 DEL 指令一直無法執(zhí)行,導(dǎo)致鎖無法釋放,該資源將會永遠(yuǎn)被鎖住。

如何用Redis分布式鎖才能確保萬無一失

所以,在使用 SETNX 拿到鎖以后,必須給 key 設(shè)置一個過期時間,以保證即使沒有被顯式釋放,在獲取鎖達(dá)到一定時間后也要自動釋放,防止資源被長時間獨占。由于 SETNX 不支持設(shè)置過期時間,所以需要額外的 EXPIRE 指令,整個過程如下:

如何用Redis分布式鎖才能確保萬無一失

這樣實現(xiàn)的分布式鎖仍然存在一個嚴(yán)重的問題,由于 SETNX 和 EXPIRE 這兩個操作是非原子性的, 如果進(jìn)程在執(zhí)行 SETNX 和  EXPIRE 之間發(fā)生異常,SETNX 執(zhí)行成功,但 EXPIRE  沒有執(zhí)行,導(dǎo)致這把鎖變得“長生不老”,這種情況就可能出現(xiàn)前文提到的鎖超時問題,其他進(jìn)程無法正常獲取鎖。

如何用Redis分布式鎖才能確保萬無一失

2)使用 SET 擴(kuò)展指令

為了解決 SETNX 和 EXPIRE 兩個操作非原子性的問題,可以使用 Redis 的 SET 指令的擴(kuò)展參數(shù),使得 SETNX 和 EXPIRE 這兩個操作可以原子執(zhí)行,整個過程如下:

如何用Redis分布式鎖才能確保萬無一失

在這個 SET 指令中:

  • NX 表示只有當(dāng) lock_resource_id 對應(yīng)的 key 值不存在的時候才能 SET 成功。保證了只有第一個請求的客戶端才能獲得鎖,而其它客戶端在鎖被釋放之前都無法獲得鎖。

  • EX 10 表示這個鎖10秒鐘后會自動過期,業(yè)務(wù)可以根據(jù)實際情況設(shè)置這個時間的大小。

但是這種方式仍然不能徹底解決分布式鎖超時問題:

  • 鎖被提前釋放。假如線程 A 在加鎖和釋放鎖之間的邏輯執(zhí)行的時間過長(或者線程 A  執(zhí)行過程中被堵塞),以至于超出了鎖的過期時間后進(jìn)行了釋放,但線程 A 在臨界區(qū)的邏輯還沒有執(zhí)行完,那么這時候線程 B  就可以提前重新獲取這把鎖,導(dǎo)致臨界區(qū)代碼不能嚴(yán)格的串行執(zhí)行。

  • 鎖被誤刪。假如以上情形中的線程A執(zhí)行完后,它并不知道此時的鎖持有者是線程 B,線程A會繼續(xù)執(zhí)行 DEL 指令來釋放鎖,如果線程 B 在臨界區(qū)的邏輯還沒有執(zhí)行完,線程 A 實際上釋放了線程 B 的鎖。

為了避免以上情況,建議不要在執(zhí)行時間過長的場景中使用 Redis 分布式鎖,同時一個比較安全的做法是在執(zhí)行 DEL 釋放鎖之前對鎖進(jìn)行判斷,驗證當(dāng)前鎖的持有者是否是自己。

具體實現(xiàn)就是在加鎖時將 value 設(shè)置為一個唯一的隨機(jī)數(shù)(或者線程 ID ),釋放鎖時先判斷隨機(jī)數(shù)是否一致,然后再執(zhí)行釋放操作,確保不會錯誤地釋放其它線程持有的鎖,除非是鎖過期了被服務(wù)器自動釋放,整個過程如下:

如何用Redis分布式鎖才能確保萬無一失

但判斷 value 和刪除 key 是兩個獨立的操作,并不是原子性的,所以這個地方需要使用 Lua 腳本進(jìn)行處理,因為 Lua 腳本可以保證連續(xù)多個指令的原子性執(zhí)行。

如何用Redis分布式鎖才能確保萬無一失

如何用Redis分布式鎖才能確保萬無一失

基于 Redis 單節(jié)點的分布式鎖基本完成了,但是這并不是一個完美的方案,只是相對完全一點,因為它并沒有完全解決當(dāng)前線程執(zhí)行超時鎖被提前釋放后,其它線程乘虛而入的問題。

3)使用 Redisson 的分布式鎖

怎么能解決鎖被提前釋放這個問題呢?

可以利用鎖的可重入特性,讓獲得鎖的線程開啟一個定時器的守護(hù)線程,每 expireTime/3 執(zhí)行一次,去檢查該線程的鎖是否存在,如果存在則對鎖的過期時間重新設(shè)置為 expireTime,即利用守護(hù)線程對鎖進(jìn)行“續(xù)命”,防止鎖由于過期提前釋放。

當(dāng)然業(yè)務(wù)要實現(xiàn)這個守護(hù)進(jìn)程的邏輯還是比較復(fù)雜的,可能還會出現(xiàn)一些未知的問題。

目前互聯(lián)網(wǎng)公司在生產(chǎn)環(huán)境用的比較廣泛的開源框架 Redisson 很好地解決了這個問題,非常的簡便易用,且支持 Redis 單實例、Redis M-S、Redis Sentinel、Redis Cluster 等多種部署架構(gòu)。

感興趣的朋友可以查閱下官方文檔或者源碼:

https://github.com/redisson/redisson/wiki

其實現(xiàn)原理如圖所示(圖中以 Redis 集群為例):

如何用Redis分布式鎖才能確保萬無一失

2、基于Redis多機(jī)實現(xiàn)的分布式鎖Redlock

以上幾種基于 Redis 單機(jī)實現(xiàn)的分布式鎖其實都存在一個問題,就是加鎖時只作用在一個 Redis 節(jié)點上,即使 Redis 通過  Sentinel 保證了高可用,但由于 Redis 的復(fù)制是異步的,Master  節(jié)點獲取到鎖后在未完成數(shù)據(jù)同步的情況下發(fā)生故障轉(zhuǎn)移,此時其他客戶端上的線程依然可以獲取到鎖,因此會喪失鎖的安全性。

整個過程如下:

  • 客戶端 A 從 Master 節(jié)點獲取鎖。

  • Master 節(jié)點出現(xiàn)故障,主從復(fù)制過程中,鎖對應(yīng)的 key 沒有同步到 Slave 節(jié)點。

  • Slave升 級為 Master 節(jié)點,但此時的 Master 中沒有鎖數(shù)據(jù)。

  • 客戶端 B 請求新的 Master 節(jié)點,并獲取到了對應(yīng)同一個資源的鎖。

  • 出現(xiàn)多個客戶端同時持有同一個資源的鎖,不滿足鎖的互斥性。

正因為如此,在 Redis 的分布式環(huán)境中,Redis 的作者 antirez 提供了 RedLock 的算法來實現(xiàn)一個分布式鎖,該算法大概是這樣的:

假設(shè)有 N(N>=5)個 Redis 節(jié)點,這些節(jié)點完全互相獨立,不存在主從復(fù)制或者其他集群協(xié)調(diào)機(jī)制,確保在這N個節(jié)點上使用與在 Redis 單實例下相同的方法獲取和釋放鎖。

獲取鎖的過程,客戶端應(yīng)執(zhí)行如下操作:

  • 獲取當(dāng)前 Unix 時間,以毫秒為單位。

  • 按順序依次嘗試從5個實例使用相同的 key 和具有唯一性的 value(例如 UUID)獲取鎖。當(dāng)向 Redis  請求獲取鎖時,客戶端應(yīng)該設(shè)置一個網(wǎng)絡(luò)連接和響應(yīng)超時時間,這個超時時間應(yīng)該小于鎖的失效時間。例如鎖自動失效時間為10秒,則超時時間應(yīng)該在5-50毫秒之間。這樣可以避免服務(wù)器端  Redis 已經(jīng)掛掉的情況下,客戶端還在一直等待響應(yīng)結(jié)果。如果服務(wù)器端沒有在規(guī)定時間內(nèi)響應(yīng),客戶端應(yīng)該盡快嘗試去另外一個 Redis  實例請求獲取鎖。

  • 客戶端使用當(dāng)前時間減去開始獲取鎖時間(步驟1記錄的時間)就得到獲取鎖使用的時間。當(dāng)且僅當(dāng)從大多數(shù)(N/2+1,這里是3個節(jié)點)的 Redis 節(jié)點都取到鎖,并且使用的時間小于鎖失效時間時,鎖才算獲取成功。

  • 如果取到了鎖,key 的真正有效時間等于有效時間減去獲取鎖所使用的時間(步驟3計算的結(jié)果)。

  • 如果因為某些原因,獲取鎖失?。]有在至少N/2+1個 Redis 實例取到鎖或者取鎖時間已經(jīng)超過了有效時間),客戶端應(yīng)該在所有的 Redis 實例上進(jìn)行解鎖(使用 Redis Lua 腳本)。

釋放鎖的過程相對比較簡單:客戶端向所有 Redis 節(jié)點發(fā)起釋放鎖的操作,包括加鎖失敗的節(jié)點,也需要執(zhí)行釋放鎖的操作,antirez 在算法描述中特別強(qiáng)調(diào)這一點,這是為什么呢?

原因是可能存在某個節(jié)點加鎖成功后返回客戶端的響應(yīng)包丟失了,這種情況在異步通信模型中是有可能發(fā)生的:客戶端向服務(wù)器通信是正常的,但反方向卻是有問題的。雖然對客戶端而言,由于響應(yīng)超時導(dǎo)致加鎖失敗,但是對  Redis節(jié)點而言,SET 指令執(zhí)行成功,意味著加鎖成功。因此,釋放鎖的時候,客戶端也應(yīng)該對當(dāng)時獲取鎖失敗的那些 Redis  節(jié)點同樣發(fā)起請求。

除此之外,為了避免 Redis 節(jié)點發(fā)生崩潰重啟后造成鎖丟失,從而影響鎖的安全性,antirez 還提出了延時重啟的概念,即一個節(jié)點崩潰后不要立即重啟,而是等待一段時間后再進(jìn)行重啟,這段時間應(yīng)該大于鎖的有效時間。

關(guān)于 Redlock 的更深層次的學(xué)習(xí),感興趣的朋友可以查閱下官方文檔:https://redis.io/topics/distlock

四、總結(jié)

如何用Redis分布式鎖才能確保萬無一失

分布式系統(tǒng)設(shè)計是實現(xiàn)復(fù)雜性和收益的平衡,既要盡可能地安全可靠,也要避免過度設(shè)計。Redlock  確實能夠提供更安全的分布式鎖,但也是有代價的,需要更多的 Redis 節(jié)點。在實際業(yè)務(wù)中,一般使用基于單點的 Redis  實現(xiàn)分布式鎖就可以滿足絕大部分的需求,偶爾出現(xiàn)數(shù)據(jù)不一致的情況,可通過人工介入回補(bǔ)數(shù)據(jù)進(jìn)行解決,正所謂“技術(shù)不夠,人工來湊”!。

“如何用Redis分布式鎖才能確保萬無一失”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實用文章!

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

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

AI