深度分析Redis分散式鎖在電商超賣業務場景下的使用

奕鵬發表於2021-11-02

專注於PHP、MySQL、Linux和前端開發,感興趣的感謝點個關注喲!!!文章整理在GitHub,Gitee主要包含的技術有PHP、Redis、MySQL、JavaScript、HTML&CSS、Linux、Java、Golang、Linux和工具資源等相關理論知識、面試題和實戰內容。

前面寫了一篇關於用Redis來解決秒殺業務場景下超賣的文章,羅列了秒殺場景下,為什麼會超賣?如何解決超賣?使用Redis分散式鎖有哪些問題?提到了幾種實現技術方案。原文連結。感興趣的可以閱讀。
Snipaste_2021-10-31_21-04-15

今天繼續給大家分享一篇關於Redis分散式鎖的文章,其中的主角就是Redis作者提到的redlock。

在上一篇文章的結尾處,我當時提出了這樣一個問題。如果是叢集、主從複製和哨兵模式的部署模式情況下,Redis的分散式鎖如何保證實現高可用。大家看到此處的時,可以先思考一下,如何保證?

Redlock定義

Redlock是Redis作者針對叢集、主從複製等業務場景下,用Redis實現分散式鎖高可用的一種實現演算法,主要是保證Redis服務不可用場景下的鎖失效問題。

這種演算法具體是怎麼實現的呢?就是部署多臺與master節點同等級別的其他節點,這幾個Redis不參與其他的業務。每一個執行緒在向master節點請求鎖的同時,也向這幾個同等級別的節點傳送加鎖請求,只有當超過一半的節點數加鎖成功,此時的分散式鎖才算真正的成功。大致的邏輯圖如下:
Snipaste_2021-10-31_19-35-57

  1. 一個thread表示一個請求,當前的thread首先向master節點傳送加鎖請求。

  2. 同樣的,該thread需要向node1,node2,node3傳送加鎖請求。

  3. 只有當master節點和nodex節點返回加鎖成功,才表示當前的thread加鎖成功,否則加鎖失敗。

Redlock由來

假設我們的Redis部署架構是一主多從的模式,每一個thread都會往master節點寫入資料,讀資料都是從slave節點讀資料。大致的架構模式如下:
Snipaste_2021-10-31_19-44-59

  1. 當有一個thread執行緒向master節點加鎖成功之後,此時master節點會把加鎖的資料傳送給slave節點,其他的thread在根據slave節點中的鎖資料,判斷當前是否有鎖。如果有鎖則無法進行加鎖操作,無鎖則有且只有一個thread能夠實現加鎖成功。

    Redis的主從複製是非同步操作的,就是說客戶端在向master傳送寫資料之後,master不會馬上把寫入的資料傳送給slave節點,而是先響應客戶端寫入資料成功之後在把新寫入的資料同步給slave節點。

  2. 當然1中的描述從理論上來說是完全沒有問題的,但是我們考慮一下,如果master節點在同步資料的過程中掛了。slave升級為master節點,升級為master節點的slave節點此時是沒有鎖資料的。其他的thread肯定會進行加鎖操作。試想一下,此時整個系統只會存在一把鎖嗎?

    這裡需要注意一下,slave切換master之後,之前的master在服務恢復之後變為slave,會情況自身的所有資料。

通過上面的分析,我們就不難得出,Redis分散式鎖在高可用架構的模式下並不一定完全可靠。因此,Redlock就誕生了。

使用要求

  1. Redis的節點要選擇奇數個節點,並且獲取鎖成功的節點數量必須是 成功獲取鎖數量 >= (節點數) / 2 + 1。奇數個是為了提高加鎖成功的概念。試想一下如果是4個幾點,一半加鎖成功,一半加鎖失敗,各自佔50%的機率。只有成功超過或者失敗的概率超過50%,此時我就才好判斷是成功與否。

  2. 記錄獲取鎖的開始時間和結束時間。在判斷鎖是否成功時,要把兩個時間相減,最終確認鎖的存活時間。如果加鎖的時間大於鎖有效時長則表示加鎖失敗。如果活的存活時間過小,低於預估的業務時間,也要判斷加鎖失敗。

  3. 執行業務之後,一定要向所有節點傳送釋放鎖請求,哪怕是鎖會自動失效。因為不主動釋放鎖,在設定鎖時長過大的情況下,當前業務執行完畢之後。其他的請求仍然無法獲取到鎖。

程式碼示例

在Redloc定義中提到了實現的思路,下面使用虛擬碼演示,從程式碼層面該如何去實現。其實Redlock的加鎖邏輯和上一篇文章提到的單機加鎖邏輯都是一樣的,無非就是多了記錄加鎖時長、判斷加鎖成功與否的情況處理。

function redLock() 
{
    // 記錄加鎖開始時間(這裡簡單一點,就用秒為單位了。實際情況用毫秒記錄。)
    $lockBeginTime = time();

    // 鎖時長
    $expireTime = 3;

    $redisClient = new Redis();
    // 1. 向master節點傳送加鎖請求
    $result1 = $redisClient->set('key', 'clientId', ['nx', 'px' => $expireTime * 1000]);

    // 2. 向node1傳送加鎖請求
    $result2 = $redisClient->set('key', 'clientId', ['nx', 'px' => $expireTime * 1000]);

    // 3. 向node2傳送加鎖請求
    $result2 = $redisClient->set('key', 'clientId', ['nx', 'px' => $expireTime * 1000]);

    // 4. 向node3傳送加鎖請求
    $result3 = $redisClient->set('key', 'clientId', ['nx', 'px' => $expireTime * 1000]);

    // 記錄加鎖結束時間(實際情況下,同樣使用毫秒。)
    $lockEndTime = time();

    // 判斷加鎖是否成功
    if ($result1 && $result2 && $result3 && ($lockEndTime-$lockBeginTime) < $expireTime) {
        // 加鎖成功,執行對應的業務邏輯程式碼。
        // 釋放鎖
    } else {
        // 加鎖失敗,執行釋放鎖操作。
    }
}

一定要記住,在進行釋放鎖的時候,需要向每一個加鎖的節點傳送釋放鎖請求。

加鎖和解鎖優化

上面的示例程式碼,都是使用的同步操作去加鎖和解鎖。在這個過程中無疑是增加了時間上的成本消耗。某一個加鎖比較慢,也很容易導致加鎖失敗。因此推薦在加鎖和解鎖的過程都採用多執行緒去執行加鎖。

分散式鎖總結

羅列一下個人對分散式鎖中需要特別注意的事項做幾個總結。這幾點屬於個人總結,大家閱讀時,需要多多思考是否完全正確。

  1. 鎖安全。既然是鎖,就說明不管在任何的情況下,同一時刻,只有一個執行緒能夠獲取到資源的執行權,其他的執行緒是不能對該資源進行操作。這也可以理解為鎖互斥。

  2. 靈活性。如果某一個或者某些節點掛了,仍然能夠保證鎖的穩定性、正確性,而不是某一個節點掛了就不能正常使用了。因為在實際的生產環境中,任何意向不到的情況都有可能發生。Anything is possible, but nothing is easy.

  3. 加鎖和釋放。在使用完鎖之後,一定要記得釋放鎖。哪怕是當前系統中存不存在鎖,都不會影響業務的情況下也要及時的釋放掉資源的佔用。

Redlock現狀

通過上面的分析,我們們基本明白了Redlock的一個實現原理。可能你也會覺得這樣實現分散式鎖已經沒問題了,這樣你就大錯特錯了。當Redis作者提出該概念之後,就受到很多質疑,因為這樣實現分散式鎖也會存在很多的問題。下面羅列一些個人現目前認知水平已經能夠知道的和Redis官網的說明。後面有其他的認知,也會更新。

  1. 增加了部署成本,因為使用Redlock需要增加幾臺與master同等級的節點來實現加鎖。這幾個節點啥也不幹,就只是負責加鎖和釋放鎖邏輯。

  2. 安全爭議。這個演算法安全麼?我們可以從不同的場景討論一下。讓我們假設客戶端從大多數Redis例項取到了鎖。所有的例項都包含同樣的key,並且key的有效時間也一樣。然而,key肯定是在不同的時間被設定上的,所以key的失效時間也不是精確的相同。我們假設第一個設定的key時間是T1(開始向第一個server傳送命令前時間),最後一個設定的key時間是T2(得到最後一臺server的答覆後的時間),我們可以確認,第一個server的key至少會存活 MIN_VALIDITY=TTL-(T2-T1)-CLOCK_DRIFT{.highlighter-rouge}。所有其他的key的存活時間,都會比這個key時間晚,所以可以肯定,所有key的失效時間至少是MIN_VALIDITY。當大部分例項的key被設定後,其他的客戶端將不能再取到鎖,因為至少N/2+1個例項已經存在key。所以,如果一個鎖被(客戶端)獲取後,客戶端自己也不能再次申請到鎖(違反互相排斥屬性)。然而我們也想確保,當多個客戶端同時搶奪一個鎖時不能兩個都成功。如果客戶端在獲取到大多數redis例項鎖,使用的時間接近或者已經大於失效時間,客戶端將認為鎖是失效的鎖,並且將釋放掉已經獲取到的鎖,所以我們只需要在有效時間範圍內獲取到大部分鎖這種情況。在上面已經討論過有爭議的地方,在MIN_VALIDITY{.highlighter-rouge}時間內,將沒有客戶端再次取得鎖。所以只有一種情況,多個客戶端會在相同時間取得N/2+1例項的鎖,那就是取得鎖的時間大於失效時間(TTL time),這樣取到的鎖也是無效的。

  3. 系統活性爭議。系統的活性安全基於三個主要特性: 鎖的自動釋放(因為key失效了):最終鎖可以再次被使用。客戶端通常會將沒有獲取到的鎖刪除,或者鎖被取到後,使用完後,客戶端會主動(提前)釋放鎖,而不是等到鎖失效另外的客戶端才能取到鎖。當客戶端重試獲取鎖時,需要等待一段時間,這個時間必須大於從大多數Redis例項成功獲取鎖使用的時間,以最大限度地避免腦裂。然而,當網路出現問題時系統在失效時間(TTL){.highlighter-rouge}內就無法服務,這種情況下我們的程式就會為此付出代價。如果網路持續的有問題,可能就會出現死迴圈了。 這種情況發生在當客戶端剛取到一個鎖還沒有來得及釋放鎖就被網路隔離。如果網路一直沒有恢復,這個演算法會導致系統不可用。

本作品採用《CC 協議》,轉載必須註明作者和本文連結
喜歡的,可以關注公眾號"卡二條的技術圈"。

相關文章