天天看點

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

專注于php、mysql、linux和前端開發,感興趣的感謝點個關注喲!!!文章整理在github,gitee主要包含的技術有php、redis、mysql、javascript、html&css、linux、java、golang、linux和工具資源等相關理論知識、面試題和實戰内容。

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

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

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

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

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

這種算法具體是怎麼實作的呢?就是部署多台與master節點同等級别的其他節點,這幾個redis不參與其他的業務。每一個線程在向master節點請求鎖的同時,也向這幾個同等級别的節點發送加鎖請求,隻有當超過一半的節點數加鎖成功,此時的分布式鎖才算真正的成功。大緻的邏輯圖如下:

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

一個thread表示一個請求,目前的thread首先向master節點發送加鎖請求。

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

隻有當master節點和nodex節點傳回加鎖成功,才表示目前的thread加鎖成功,否則加鎖失敗。

假設我們的redis部署架構是一主多從的模式,每一個thread都會往master節點寫入資料,讀資料都是從slave節點讀資料。大緻的架構模式如下:

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

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

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

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

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

通過上面的分析,我們就不難得出,redis分布式鎖在高可用架構的模式下并不一定完全可靠。是以,redlock就誕生了。

redis的節點要選擇奇數個節點,并且擷取鎖成功的節點數量必須是 <code>成功擷取鎖數量 &amp;gt;= (節點數) / 2 + 1</code>。奇數個是為了提高加鎖成功的概念。試想一下如果是4個幾點,一半加鎖成功,一半加鎖失敗,各自占50%的幾率。隻有成功超過或者失敗的機率超過50%,此時我就才好判斷是成功與否。

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

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

在redloc定義中提到了實作的思路,下面使用<code>僞代碼</code>示範,從代碼層面該如何去實作。其實redlock的加鎖邏輯和上一篇文章提到的單機加鎖邏輯都是一樣的,無非就是多了記錄加鎖時長、判斷加鎖成功與否的情況處理。

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

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

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

鎖安全。既然是鎖,就說明不管在任何的情況下,<code>同一時刻</code>,隻有一個線程能夠擷取到資源的執行權,其他的線程是不能對該資源進行操作。這也可以了解為鎖互斥。

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

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

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

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

安全争議。這個算法安全麼?我們可以從不同的場景讨論一下。讓我們假設用戶端從大多數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),這樣取到的鎖也是無效的。

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

繼續閱讀