您好,登錄后才能下訂單哦!
這篇文章主要講解了“Java分布式鎖的使用方案有哪些”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“Java分布式鎖的使用方案有哪些”吧!
前言
隨著互聯網技術的不斷發展,數據量的不斷增加,業務邏輯日趨復雜,在這種背景下,傳統的集中式系統已經無法滿足我們的業務需求,分布式系統被應用在更多的場景,而在分布式系統中訪問共享資源就需要一種互斥機制,來防止彼此之間的互相干擾,以保證一致性,在這種情況下,我們就需要用到分布式鎖。
分布式一致性問題
首先我們先來看一個小例子:
假設某商城有一個商品庫存剩10個,用戶A想要買6個,用戶B想要買5個,在理想狀態下,用戶A先買走了6了,庫存減少6個還剩4個,此時用戶B應該無法購買5個,給出數量不足的提示;而在真實情況下,用戶A和B同時獲取到商品剩10個,A買走6個,在A更新庫存之前,B又買走了5個,此時B更新庫存,商品還剩5個,這就是典型的電商“秒殺”活動。
從上述例子不難看出,在高并發情況下,如果不做處理將會出現各種不可預知的后果。那么在這種高并發多線程的情況下,解決問題最有效最普遍的方法就是給共享資源或對共享資源的操作加一把鎖,來保證對資源的訪問互斥。在Java JDK已經為我們提供了這樣的鎖,利用ReentrantLcok或者synchronized,即可達到資源互斥訪問的目的。但是在分布式系統中,由于分布式系統的分布性,即多線程和多進程并且分布在不同機器中,這兩種鎖將失去原有鎖的效果,需要我們自己實現分布式鎖——分布式鎖。
分布式鎖需要具備哪些條件
獲取鎖和釋放鎖的性能要好
判斷是否獲得鎖必須是原子性的,否則可能導致多個請求都獲取到鎖
網絡中斷或宕機無法釋放鎖時,鎖必須被清楚,不然會發生死鎖
可重入一個線程中可以多次獲取同一把鎖,比如一個線程在執行一個帶鎖的方法,該方法中又調用了另一個需要相同鎖的方法,則該線程可以直接執行調用的方法,而無需重新獲得鎖;
5.阻塞鎖和非阻塞鎖,阻塞鎖即沒有獲取到鎖,則繼續等待獲取鎖;非阻塞鎖即沒有獲取到鎖后,不繼續等待,直接返回鎖失敗。
分布式鎖實現方式
一、數據庫鎖
一般很少使用數據庫鎖,性能不好并且容易產生死鎖。
基于MySQL鎖表
該實現方式完全依靠數據庫唯一索引來實現,當想要獲得鎖時,即向數據庫中插入一條記錄,釋放鎖時就刪除這條記錄。這種方式存在以下幾個問題:
(1) 鎖沒有失效時間,解鎖失敗會導致死鎖,其他線程無法再獲取到鎖,因為唯一索引insert都會返回失敗。
(2) 只能是非阻塞鎖,insert失敗直接就報錯了,無法進入隊列進行重試
(3) 不可重入,同一線程在沒有釋放鎖之前無法再獲取到鎖
采用樂觀鎖增加版本號
根據版本號來判斷更新之前有沒有其他線程更新過,如果被更新過,則獲取鎖失敗。
二、緩存鎖
具體實例可以參考我講述Redis的系列文章,里面有完整的Redis分布式鎖實現方案
這里我們主要介紹幾種基于redis實現的分布式鎖:
基于setnx、expire兩個命令來實現
基于setnx(set if not exist)的特點,當緩存里key不存在時,才會去set,否則直接返回false。如果返回true則獲取到鎖,否則獲取鎖失敗,為了防止死鎖,我們再用expire命令對這個key設置一個超時時間來避免。但是這里看似完美,實則有缺陷,當我們setnx成功后,線程發生異常中斷,expire還沒來的及設置,那么就會產生死鎖。
解決上述問題有兩種方案
第一種是采用redis2.6.12版本以后的set,它提供了一系列選項
EX seconds – 設置鍵key的過期時間,單位時秒
PX milliseconds – 設置鍵key的過期時間,單位時毫秒
NX – 只有鍵key不存在的時候才會設置key的值
XX – 只有鍵key存在的時候才會設置key的值
第二種采用setnx(),get(),getset()實現,大體的實現過程如下:
(1) 線程Asetnx,值為超時的時間戳(t1),如果返回true,獲得鎖。
(2) 線程B用get 命令獲取t1,與當前時間戳比較,判斷是否超時,沒超時false,如果已超時執行步驟3
(3) 計算新的超時時間t2,使用getset命令返回t3(這個值可能其他線程已經修改過),如果t1==t3,獲得鎖,如果t1!=t3說明鎖被其他線程獲取了
(4) 獲取鎖后,處理完業務邏輯,再去判斷鎖是否超時,如果沒超時刪除鎖,如果已超時,不用處理(防止刪除其他線程的鎖)
RedLock算法
redlock算法是redis作者推薦的一種分布式鎖實現方式,算法的內容如下:
(1) 獲取當前時間;
(2) 嘗試從5個相互獨立redis客戶端獲取鎖;
(3) 計算獲取所有鎖消耗的時間,當且僅當客戶端從多數節點獲取鎖,并且獲取鎖的時間小于鎖的有效時間,認為獲得鎖;
(4) 重新計算有效期時間,原有效時間減去獲取鎖消耗的時間;
(5) 刪除所有實例的鎖
redlock算法相對于單節點redis鎖可靠性要更高,但是實現起來條件也較為苛刻。
(1) 必須部署5個節點才能讓Redlock的可靠性更強。
(2) 需要請求5個節點才能獲取到鎖,通過Future的方式,先并發向5個節點請求,再一起獲得響應結果,能縮短響應時間,不過還是比單節點redis鎖要耗費更多時間。
然后由于必須獲取到5個節點中的3個以上,所以可能出現獲取鎖沖突,即大家都獲得了1-2把鎖,結果誰也不能獲取到鎖,這個問題,redis作者借鑒了raft算法的精髓,通過沖突后在隨機時間開始,可以大大降低沖突時間,但是這問題并不能很好的避免,特別是在第一次獲取鎖的時候,所以獲取鎖的時間成本增加了。
如果5個節點有2個宕機,此時鎖的可用性會極大降低,首先必須等待這兩個宕機節點的結果超時才能返回,另外只有3個節點,客戶端必須獲取到這全部3個節點的鎖才能擁有鎖,難度也加大了。
如果出現網絡分區,那么可能出現客戶端永遠也無法獲取鎖的情況,介于這種情況,下面我們來看一種更可靠的分布式鎖zookeeper鎖。
zookeeper分布式鎖
關于zookeeper的分布式鎖實現在之前講述zookeeper的時候已經介紹了。這里不再贅述、
首先我們來了解一下zookeeper的特性,看看它為什么適合做分布式鎖,
zookeeper是一個為分布式應用提供一致服務的軟件,它內部是一個分層的文件系統目錄樹結構,規定統一個目錄下只能有一個唯一文件名。
數據模型:
永久節點:節點創建后,不會因為會話失效而消失
臨時節點:與永久節點相反,如果客戶端連接失效,則立即刪除節點
順序節點:與上述兩個節點特性類似,如果指定創建這類節點時,zk會自動在節點名后加一個數字后綴,并且是有序的。
監視器(watcher):
當創建一個節點時,可以注冊一個該節點的監視器,當節點狀態發生改變時,watch被觸發時,ZooKeeper將會向客戶端發送且僅發送一條通知,因為watch只能被觸發一次。
根據zookeeper的這些特性,我們來看看如何利用這些特性來實現分布式鎖:
創建一個鎖目錄lock
希望獲得鎖的線程A就在lock目錄下,創建臨時順序節點
獲取鎖目錄下所有的子節點,然后獲取比自己小的兄弟節點,如果不存在,則說明當前線程順序號最小,獲得鎖
線程B獲取所有節點,判斷自己不是最小節點,設置監聽(watcher)比自己次小的節點(只關注比自己次小的節點是為了防止發生“羊群效應”)
線程A處理完,刪除自己的節點,線程B監聽到變更事件,判斷自己是最小的節點,獲得鎖。
感謝各位的閱讀,以上就是“Java分布式鎖的使用方案有哪些”的內容了,經過本文的學習后,相信大家對Java分布式鎖的使用方案有哪些這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。