Redis 分布式鎖進化史(解讀 缺陷分析)

2022-06-06 13:51:10 字數 4315 閱讀 6708

redis分布式鎖進化史

近兩年來微服務變得越來越熱門,越來越多的應用部署在分布式環境中,在分布式環境中,資料一致性是一直以來需要關注並且去解決的問題,分布式鎖也就成為了一種廣泛使用的技術,常用的分布式實現方式為redis,zookeeper,其中基於redis的分布式鎖的使用更加廣泛。

但是在工作和網路上看到過各個版本的redis分布式鎖實現,每種實現都有一些不嚴謹的地方,甚至有可能是錯誤的實現,包括在**中,如果不能正確的使用分布式鎖,可能造成嚴重的生產環境故障,本文主要對目前遇到的各種分布式鎖以及其缺陷做了乙個整理,並對如何選擇合適的redis分布式鎖給出建議。

各個版本的redis分布式鎖

trylock() release()
這個版本應該是最簡單的版本,也是出現頻率很高的乙個版本,首先給鎖加乙個過期時間操作是為了避免應用在服務重啟或者異常導致鎖無法釋放後,不會出現鎖一直無法被釋放的情況。

這個方案的乙個問題在於每次提交乙個redis請求,如果執行完第一條命令後應用異常或者重啟,鎖將無法過期,一種改善方案就是使用lua指令碼(包含setnx和expire兩條命令),但是如果redis僅執行了一條命令後crash或者發生主從切換,依然會出現鎖沒有過期時間,最終導致無法釋放。

另外乙個問題在於,很多同學在釋放分布式鎖的過程中,無論鎖是否獲取成功,都在finally中釋放鎖,這樣是乙個鎖的錯誤使用,這個問題將在後續的v3.0版本中解決。

針對鎖無法釋放問題的乙個解決方案基於getset命令來實現

trylock()else } } } release()
思路:

setnx(key,expiretime)獲取鎖

如果獲取鎖失敗,通過get(key)返回的時間戳檢查鎖是否已經過期

getset(key,expiretime)修改value為newexpiretime

檢查getset返回的舊值,如果等於get返回的值,則認為獲取鎖成功

注意:這個版本去掉了expire命令,改為通過value時間戳值來判斷過期
問題:

1. 在鎖競爭較高的情況下,會出現value不斷被覆蓋,但是沒有乙個client獲取到鎖

2. 在獲取鎖的過程中不斷的修改原有鎖的資料,設想一種場景c1,c2競爭鎖,c1獲取到了鎖,c2鎖執行了getset操作修改了c1鎖的過期時間,如果c1沒有正確釋放鎖,鎖的過期時間被延長,其它client需要等待更久的時間

trylock() release()
redis 2.6.12版本後setnx增加過期時間引數,這樣就解決了兩條命令無法保證原子性的問題。但是設想下面乙個場景: 

1. c1成功獲取到了鎖,之後c1因為gc進入等待或者未知原因導致任務執行過長,最後在鎖失效前c1沒有主動釋放鎖 2. c2在c1的鎖超時後獲取到鎖,並且開始執行,這個時候c1和c2都同時在執行,會因重複執行造成資料不一致等未知情況 3. c1如果先執行完畢,則會釋放c2的鎖,此時可能導致另外乙個c3程序獲取到了鎖

大致的流程圖

存在問題:

1. 由於c1的停頓導致c1 和c2同都獲得了鎖並且同時在執行,在業務實現間接要求必須保證冪等性

2. c1釋放了不屬於c1的鎖

trylock() release()
這個方案通過指定value為時間戳,並在釋放鎖的時候檢查鎖的value是否為獲取鎖的value,避免了v2.0版本中提到的c1釋放了c2持有的鎖的問題;另外在釋放鎖的時候因為涉及到多個redis操作,並且考慮到check and set 模型的併發問題,所以使用lua指令碼來避免併發問題。

存在問題:

如果在併發極高的場景下,比如搶紅包場景,可能存在unixtimestamp重複問題,另外由於不能保證分布式環境下的物理時鐘一致性,也可能存在unixtimestamp重複問題,只不過極少情況下會遇到。
trylock() release()
redis 2.6.12後set同樣提供了乙個nx引數,等同於setnx命令,官方文件上提醒後面的版本有可能去掉setnx, setex, psetex,並用set命令代替,另外乙個優化是使用乙個自增的唯一uniqid代替時間戳來規避v3.0提到的時鐘問題。

這個方案是目前最優的分布式鎖方案,但是如果在redis集群環境下依然存在問題:

由於redis集群資料同步為非同步,假設在master節點獲取到鎖後未完成資料同步情況下master節點crash,此時在新的master節點依然可以獲取鎖,所以多個client同時獲取到了鎖

分布式redis鎖:redlock

v3.1的版本僅在單例項的場景下是安全的,針對如何實現分布式redis的鎖,國外的分布式專家有過激烈的討論, antirez提出了分布式鎖演算法redlock,在distlock話題下可以看到對redlock的詳細說明,下面是redlock演算法的乙個中文說明(引用)

假設有n個獨立的redis節點

獲取當前時間(毫秒數)。

按順序依次向n個redis節點執行獲取鎖的操作。這個獲取操作跟前面基於單redis節點的獲取鎖的過程相同,包含隨機字串my_random_value,也包含過期時間(比如px 30000,即鎖的有效時間)。為了保證在某個redis節點不可用的時候演算法能夠繼續執行,這個獲取鎖的操作還有乙個超時時間(time out),它要遠小於鎖的有效時間(幾十毫秒量級)。客戶端在向某個redis節點獲取鎖失敗以後,應該立即嘗試下乙個redis節點。這裡的失敗,應該包含任何型別的失敗,比如該redis節點不可用,或者該redis節點上的鎖已經被其它客戶端持有(注:redlock原文中這裡只提到了redis節點不可用的情況,但也應該包含其它的失敗情況)。

計算整個獲取鎖的過程總共消耗了多長時間,計算方法是用當前時間減去第1步記錄的時間。如果客戶端從大多數redis節點(>= n/2+1)成功獲取到了鎖,並且獲取鎖總共消耗的時間沒有超過鎖的有效時間(lock validity time),那麼這時客戶端才認為最終獲取鎖成功;否則,認為最終獲取鎖失敗。

如果最終獲取鎖成功了,那麼這個鎖的有效時間應該重新計算,它等於最初的鎖的有效時間減去第3步計算出來的獲取鎖消耗的時間。

如果最終獲取鎖失敗了(可能由於獲取到鎖的redis節點個數少於n/2+1,或者整個獲取鎖的過程消耗的時間超過了鎖的最初有效時間),那麼客戶端應該立即向所有redis節點發起釋放鎖的操作(即前面介紹的redis lua指令碼)。

釋放鎖:對所有的redis節點發起釋放鎖操作

然而martin kleppmann針對這個演算法提出了質疑,提出應該基於fencing token機制(每次對資源進行操作都需要進行token驗證)

1. redlock在系統模型上尤其是在分布式時鐘一致性問題上提出了假設,實際場景下存在時鐘不一致和時鐘跳躍問題,而redlock恰恰是基於timing的分布式鎖

2. 另外redlock由於是基於自動過期機制,依然沒有解決長時間的gc pause等問題帶來的鎖自動失效,從而帶來的安全性問題。

接著antirez又回覆了martin kleppmann的質疑,給出了過期機制的合理性,以及實際場景中如果出現停頓問題導致多個client同時訪問資源的情況下如何處理。

針對redlock的問題,基於redis的分布式鎖到底安全嗎給出了詳細的中文說明,並對redlock演算法存在的問題提出了分析。

總結

不論是基於setnx版本的redis單例項分布式鎖,還是redlock分布式鎖,都是為了保證下特性

1. 安全性:在同一時間不允許多個client同時持有鎖

2. 活性

死鎖:鎖最終應該能夠被釋放,即使client端crash或者出現網路分割槽(通常基於超時機制)

容錯性:只要超過半數redis節點可用,鎖都能被正確獲取和釋放

所以在開發或者使用分布式鎖的過程中要保證安全性和活性,避免出現不可**的結果。

另外每個版本的分布式鎖都存在一些問題,在鎖的使用上要針對鎖的實用場景選擇合適的鎖,通常情況下鎖的使用場景包括:

efficiency(效率):只需要乙個client來完成操作,不需要重複執行,這是乙個對寬鬆的分布式鎖,只需要保證鎖的活性即可;

correctness(正確性):多個client保證嚴格的互斥性,不允許出現同時持有鎖或者對同時操作同一資源,這種場景下需要在鎖的選擇和使用上更加嚴格,同時在業務**上盡量做到冪等

在redis分布式鎖的實現上還有很多問題等待解決,我們需要認識到這些問題並清楚如何正確實現乙個redis 分布式鎖,然後在工作中合理的選擇和正確的使用分布式鎖。

redis分布式鎖

redis分布式鎖 直接上 我寫了四個redis分布式鎖的方法,大家可以提個意見 第一種方法 redis分布式鎖 param timeout public void lock long timeout thread.sleep 100 catch exception e override publi...

Redis分布式鎖

分布式鎖一般有三種實現方式 1.資料庫樂觀鎖 2.基於redis的分布式鎖 3.基於zookeeper的分布式鎖.首先,為了確保分布式鎖可用,我們至少要確保鎖的實現同時滿足以下四個條件 互斥性。在任意時刻,只有乙個客戶端能持有鎖。不會發生死鎖。即使有乙個客戶端在持有鎖的期間崩潰而沒有主動解鎖,也能保...

redis分布式鎖

使用redis的setnx命令實現分布式鎖 redis為單程序單執行緒模式,採用佇列模式將併發訪問變成序列訪問,且多個客戶端對redis的連線並不存在競爭關係。redis的setnx命令可以方便的實現分布式鎖。setnx key value 將key的值設為value,當且僅當key不存在。如給定的...