深入淺出探索Java分布式鎖原理
什么是分布式鎖?它能干什么?
相信大家對于Java
提供的synchronized
關(guān)鍵字以及Lock
鎖都不陌生,在實際的項目中大家都使用過。如下圖所示,在同一個JVM
進程中,Thread1
獲得鎖之后,對共享資源進行操作,其他線程未獲得鎖的線程只能等待Thread1
釋放后才能進行對應的操作。
但是隨著業(yè)務的不斷發(fā)展,原先的單體應用被拆分為多個微服務,每個微服務又會部署多個實例,于是就形成了當下的微服務架構(gòu)。處理共享資源的請求來自不同的服務實例,也就是在不同的JVM
進程中。原先的單體服務中的加鎖方式在分布式場景下不能滿足共享資源的并發(fā)訪問要求。因此我們需要一種適用于分布式場景下的共享資源安全的處理機制,此時應對這種問題的分布式鎖就應運而生了。
既然JVM
進程管不到其他服務實例的線程,那么可以借助于外部組件能力來實現(xiàn)不同服務實例對于共享資源的統(tǒng)一管控,這種能力我們可以稱之為分布式鎖。因此分布式鎖的本質(zhì)就是在不同服務實例之外建立一種獲取鎖的機制,形成一種并發(fā)互斥能力來確保不同線程對于共享資源的并發(fā)安全,從而實現(xiàn)在微服務架構(gòu)中同一時刻只有一個線程可以對共享資源進行操作。對于分布式鎖來說,實際就是需要一個外部的狀態(tài)存儲系統(tǒng)來實現(xiàn)原子化的排他性操作。
通過對于分布式鎖的需求分析,總結(jié)了如下的分布式鎖四大特性,分別是多節(jié)點、加鎖速度快、排他性以及鎖過期實現(xiàn)機制。
分布式鎖實現(xiàn)方案
基于數(shù)據(jù)庫的分布式鎖實現(xiàn)方案
實現(xiàn)原理
通過數(shù)據(jù)庫的方式實現(xiàn)分布式鎖的效果,實際就是借助于數(shù)據(jù)庫的唯一性約束特性或者for update
來實現(xiàn)。這里以唯一性約束來舉個栗子,在電商領域的庫存服務負責對商品的庫存進行扣減,首先創(chuàng)建一張專門存放鎖信息的鎖表,那么庫存服務在進行庫存操作之前,先向數(shù)據(jù)庫中的鎖表插入一條鎖資源數(shù)據(jù)。
create table ‘distributed_lock' ( ‘id' BIGINT NOT NULL AUTO_INCREMENT, ‘resource_lock_key‘ varchar(64) NOT NULL PRIMARY KEY(‘id'), UNIQUE KEY ‘uk_resource_lock_key‘ (‘resource_lock_key‘) USING BTREE )
大致的交互流程如下:
1、當庫存服務進行手機庫存扣減的時候,首先先向數(shù)據(jù)庫中的鎖表當中插入一條資源鎖信息;
2、如果插入成功,則表示庫存服務1可以對手機庫存進行庫存扣減操作;
3、此時庫存服務2也要對庫存進行操作,于是同樣插入數(shù)據(jù)到鎖表中;
4、但是由于鎖表設置了唯一性約束,鎖信息插入失敗,庫存服務進行等待;
5、庫存服務1執(zhí)行完庫存扣減之后,刪除鎖表的信息;
6、庫存服務2嘗試插入資源鎖信息,發(fā)現(xiàn)可以插入成功,繼續(xù)執(zhí)行后續(xù)操作。
方案分析
基于數(shù)據(jù)庫的實現(xiàn)方式,看起來還是比較容易理解的。但是實際上還是有一些問題存在的,我們一起來分析下。
1、性能問題:由于是插入數(shù)據(jù)數(shù)據(jù)需要落盤存儲,如果平凡進行讀寫的話會影響數(shù)據(jù)庫性能,另外由于使用唯一鍵進行判斷也會一定程度上影響數(shù)據(jù)庫性能,因此數(shù)據(jù)庫方案適用于并發(fā)量不到的簡單場景;
2、數(shù)據(jù)庫如果單點部署的話會存在單點故障問題,如果數(shù)據(jù)庫出現(xiàn)故障,可能會導致平臺中的業(yè)務異常;
3、死鎖問題:在上文介紹中,包含了插入數(shù)據(jù)庫的獲取鎖的步驟,還包含了刪除鎖信息的釋放鎖的過程,但是如果庫存服務1在加鎖之后掛掉了,無法進行鎖的釋放,而其他服務又無法獲取到鎖就會造成死鎖的問題。當然了我們可以通過一個定時任務去檢查鎖表中是不是有過時的鎖資源。但是這樣無疑增加了分布式鎖實現(xiàn)的復雜性。
4、不支持可重入:如果想要實現(xiàn)可重入鎖,還需要增加主機、線程名等字段來進行標注,通過這幾個字段來判斷和當前信息是否一致,如果一致則認為已經(jīng)獲取到了鎖。 鑒于以上的這些問題,有沒有其他的分布式實現(xiàn)方案可以避免上述存在的問題呢?我們再往下來看。
基于Redis的分布式鎖實現(xiàn)方案
基于sentnx命令的實現(xiàn)原理
Redis
作為一塊高性能的數(shù)據(jù)庫中間件,經(jīng)常被當做緩存在項目中使用。因此通過Redis
實現(xiàn)分布式鎖,也是比較常見的實現(xiàn)方案。 一樣的道理,通過Redis
實現(xiàn)分布式鎖也需要通過它實現(xiàn)鎖的互斥的能力。實際上就是利用了sentnx(set if not exists)
命令。同時該命令是否能夠設置成功,決定服務是否可以拿到對應的分布式鎖。
127.0.0.1:6379> setnx stockLock 10.12.35.12_stockService
(integer) 1
如上圖所示,大致的加鎖以及釋放鎖的過程其實和數(shù)據(jù)庫的分布式鎖方案還是比較類似的。只不過將其中向數(shù)據(jù)庫插入數(shù)據(jù)的步驟替換成了向Redis
獲取鎖的步驟,由于Redis
是基于內(nèi)存進行操作的,因此性能上比基于數(shù)據(jù)庫的分布式鎖方案更好一點。
方案分析
上述基于Redis
的方案的方案在性能上具有優(yōu)勢,我們再來分析下,這個使用命令的方式有沒有什么問題。實際上和前面的數(shù)據(jù)庫方案類似,Redis
也會有死鎖問題,當獲取鎖之后如果庫存服務1掛掉了,庫存服務2就獲取不到鎖了。因此我們要對其進行優(yōu)化。那么問題的本質(zhì)是如何讓鎖可以釋放,因此我們需要在設置鎖的時候加上過期時間,這樣即使庫存服務1掛了,無法主動釋放鎖,那么到了過期時間后鎖失效,庫存服務2依然可以獲取鎖,不會再造成死鎖問題。
另外還應該注意的是,在我們設置鎖的時候,還需要帶有自身服務的業(yè)務屬性,否則容易造成錯亂。為什么這么說呢?舉個栗子,庫存服務在加完鎖之后開始執(zhí)行扣減庫存的任務,當扣減庫存完成之后,服務掛了,原先需要刪除的鎖資源,等到過期之后被Redis
刪除,此時庫存服務2可以繼續(xù)申請鎖,如果此時庫存服務1恢復了,它并不知道鎖資源已經(jīng)釋放,起來后立馬刪除了庫存服務2加的鎖,那么此時就會出現(xiàn)兩個問題:
1、庫存服務執(zhí)行完庫存扣減之后,回頭來進行鎖資源釋放的時候,發(fā)現(xiàn)鎖實際已經(jīng)不在了;
2、當庫存服務1恢復后發(fā)現(xiàn)鎖還在,立馬刪除了該鎖,完成了它掛掉之前未完成的工作。但是實際上這個鎖是庫存服務2加的鎖,如果此時庫存服務3也要嘗試加鎖,發(fā)現(xiàn)可以加鎖成功,和庫存服務2一樣同樣對庫存進行操作,那么此時就會出現(xiàn)線程安全問題。
經(jīng)過上文的分析,這個問題的根源就是在加鎖的時候沒有具體區(qū)分到底是哪個服務加的鎖。因此在執(zhí)行命令的時候,我們需要將帶有服務實例關(guān)聯(lián)屬性的設置為value
,這樣在進行鎖獲取的時候檢查下當前鎖的持有者是誰,如果不是服務實例自己則不能執(zhí)行刪除操作。
那這樣是不是就完美解決問題了呢?實際上還是有問題存在的,有同學會說,怎么這么多問題?實際上這種方案的實現(xiàn)就是在各種不完美的方案中逐漸找到相對完美的方案。
上文提到的獲取鎖判斷是不是自己方服務實例加的鎖,再執(zhí)行刪除鎖的過程實際并不是原子的。因此還是會出現(xiàn)并發(fā)安全問題,這個問題可以通過lua
腳本來解決,在lua
腳本中實現(xiàn)這個邏輯,而不是在客戶端中實現(xiàn)。 但是實際上還是有問題沒有解決,比如說我們在加鎖的時候會設置過期時間,但是過期時間應該設置多長時間呢?設置短了的話,出現(xiàn)網(wǎng)絡超時或者服務還沒有執(zhí)行完業(yè)務,鎖就失效了。設置長了話,其他服務節(jié)點等待獲取鎖的時間就會變長,降低了服務的性能。
基于Redisson實現(xiàn)
Redisson
實際上就是一個封裝了Redis
操作的客戶端,實現(xiàn)了對于常見的Redis
操作的封裝。如對于Redis
的設置鎖的步驟以及刪除鎖的步驟都進行了封裝。在設置鎖的操作中,還引入了自動給鎖續(xù)期的機制,SDK
檢測到業(yè)務未完成,但是鎖要到期后,執(zhí)行定續(xù)期。這樣并可以動態(tài)的調(diào)節(jié)過期時間,避免鎖在業(yè)務未完成情況下被釋放的問題。
同時還封裝了刪除鎖的時候執(zhí)行的業(yè)務判斷后再刪除的邏輯,這樣我們在使用Redisson
操作Redis
的時候,就和我們使用JDK
一樣。
RedLock
為了解決Redis作為分布式鎖存在的單點問題,Redis的作者又提出了Redlock的解決方案,該解決方案依賴多個Redis的Master節(jié)點,官方推薦使用5個Master節(jié)點,他們彼此之間是獨立的。大致的交互步驟如下所示:
1、首先獲取當前節(jié)點的系統(tǒng)時間;
2、客戶端嘗試向所有的Redis
實例順序地發(fā)送加鎖的請求(官方推薦Redis
集群至少5個實例),在設置鎖的過程中,使用相同的key以及隨機值value,同時請求的超時時間需要遠小于鎖的有效時間。這樣做的目的是為了防止節(jié)點不可用的時候?qū)е抡埱箧i的時候被阻塞,當實例沒響應的時候可以快速跳過,向下一個節(jié)點繼續(xù)請求鎖。
3、假設Redis集群規(guī)模為5,那么如果客戶端在大多數(shù)實例中(超過3個實例)獲得了鎖,同時計算了當前的時間減去步驟1中獲得的時間,這個事件差如果小于鎖的有效時間,那么此時可以認為加鎖成功,可以操作執(zhí)行后續(xù)的業(yè)務;
4、如果不滿足步驟3是條件,那么就表示加鎖失敗,客戶端需要向所有的Redis節(jié)點發(fā)起鎖釋放請求。
方案分析
為什么Redlock
要在集群中多個實例上加鎖呢?實際目的是通過鎖的冗余來實現(xiàn)分布式鎖的高容錯性。試想一下如果只有一個Redis
實例,一旦它掛掉了,客戶端就無法進行加鎖操作了或者鎖信息就會丟失,影響業(yè)務功能。通過在集群中多實例中冗余鎖信息,即使出現(xiàn)Redis
掛了的情況,其他節(jié)點中依然存在鎖信息,從而提升了分布式鎖的可用性。
那么為什么還要計算幾所時間呢?由于我們加鎖的時候,每個節(jié)點都設置了超時時間,如果整個加鎖的時間過長,整個過程的累加時間超過了鎖的有效時間,那么加鎖完成之后就會哦出現(xiàn)鎖失效的情況了,因此我們需要確保加鎖的事件盡可能的短,這也是為什么加鎖請求都有超時時間的原因了,發(fā)現(xiàn)超時立馬跳到下一個節(jié)點,避免單個節(jié)點耗時過長。
雖然Redlock看上去是比較完善的分布式解決方案,但是實際上這個方案是比較重的,需要維護一個Redis集群,另外過程中依賴系統(tǒng)時間,但是如果出現(xiàn)了時間跳變,那么對于整個分布式鎖都有非常大的影響。
基于Zookeeper的分布式鎖實現(xiàn)方案
實現(xiàn)原理
Zookeeper
是一個分布式的應用協(xié)調(diào)服務中間件,通過它也可以實現(xiàn)分布式鎖的效果,這里介紹的是基于臨時有序的ZNode
分布式鎖實現(xiàn)方案。在介紹方案之前,先補充下Zookeeper
中和分布式鎖息息相關(guān)的特性。
我們來看下Zookeeper
的數(shù)據(jù)結(jié)構(gòu),實際上它是一種樹形模型,類似于Linux
的文件系統(tǒng)。Zookeeper
使用類似于文件目錄的層級目錄數(shù)據(jù)結(jié)構(gòu)來組織自身的數(shù)據(jù)存儲節(jié)點,這些節(jié)點就被稱作為ZNode
,每個節(jié)點都用一個以斜杠(/)分隔的路徑來表示,而且每個節(jié)點都有父節(jié)點(根節(jié)點除外)。另外在Zookeeper
中,如果我們使用不同的創(chuàng)建參數(shù),可以創(chuàng)建不同類型的ZNode
。 1、持久化ZNode
:當createMode
為PERSISTENT
會創(chuàng)建持久化ZNode
,節(jié)點存儲的數(shù)據(jù)會永久保存在Zookeeper
中,如果createMode
為PERSISTENT_SEQUENTIAL
,則會創(chuàng)建有序持久化ZNode
,和之前的持久化節(jié)點不通的是,有序持久化節(jié)點的節(jié)點名稱會附加上全局有序的遞增序號; 2、臨時ZNode
:當createMode
為EPHEMERAL
時,創(chuàng)建的節(jié)點臨時節(jié)點,在與客戶端的session
過期后,對應的臨時節(jié)點也會被刪除。當createMode
為EPHEMERAL_SEQUENTIAL
時創(chuàng)建出來的為有序的臨時節(jié)點,當session
過期之后,節(jié)點及其存儲的數(shù)據(jù)也是會被刪除的。
通過上述對于節(jié)點特性的描述,可以看出來它的全局遞增有序以及過期刪除的特性與分布式鎖實現(xiàn)的原理非常契合。因此通過Zookeeper
實現(xiàn)分布式鎖的大致可以分為以下幾個步驟:
1、首先創(chuàng)建一個持久化節(jié)點也就是父節(jié)點,這個持久化節(jié)點代表著一個分布式鎖實例;
2、當有線程想要申請分布式鎖的時候,則在該持久化節(jié)點下創(chuàng)建臨時有序節(jié)點;
3、如果此時新建的臨時有序節(jié)點是該父節(jié)點小所有有序節(jié)點中序號最小的節(jié)點,那么此時就表示申請到了分布式鎖;
4、如果新建的臨時節(jié)點當前不是最小序號的節(jié)點,則需要不斷檢查是否最小,知道最終獲取到鎖,或者節(jié)點超時。實際上這個是通過Zookeeper
的watch
機制實現(xiàn)的,在當前節(jié)點的上一序號的節(jié)點設置監(jiān)聽器,檢查是否為最小節(jié)點的任務可以一直阻塞,直到收到上一節(jié)點被刪除的時間事件,則喚醒檢查事件,檢查當前節(jié)點是不是最小序號節(jié)點。
5、當線程執(zhí)行完業(yè)務之后,可以手動刪除該臨時節(jié)點以便于釋放持有的鎖。另外即使服務掛掉,由于對應的session失效,對應的臨時節(jié)點也會被刪除,防止出現(xiàn)死鎖問題。
和Redisson
類似,我們在實際使用Zookeeper
作為分布式鎖的時候可以用Curator
來作為開發(fā)SDK
,它同樣封裝了很多實現(xiàn),包括可重入鎖的實現(xiàn),減輕了使用者的負擔。
方案分析
看上去通過Zookeeper
實現(xiàn)分布式鎖還是比較好的一種解決方案,但是它是完美的嗎?從上面的分布式鎖的流程可知,客戶端線程想要獲取鎖就需要創(chuàng)建臨時節(jié)點,這個時候客戶端和Zookeeper
之間就會維護一個session
,來表示該客戶端還在排隊等待獲取鎖。因此這個方案的潛在問題就在于一旦出現(xiàn)網(wǎng)絡異常,或者客戶端發(fā)生STW GC
,那么就可能導致session
關(guān)閉,從而導致臨時節(jié)點被關(guān)閉,此時就會出現(xiàn)原來客戶端持有的鎖被刪除了,如果有另外的客戶端過來加鎖的話可以成功獲取,那么此時就出現(xiàn)并發(fā)安全問題了。因此在這種極端條件下,Zookeeper
的分布式鎖實現(xiàn)方案也不是100%保證安全的。
另外實際上還有基于etcd
的分布式鎖實現(xiàn)方案,其基本原理和Zookeeper
差不多,感興趣的同學可以再進行了解下。
分布式鎖方案到底選哪個?
通過上述幾種分布式鎖方案原理的闡述以及問題分析,每個方案都有自己的長處以及缺點。所以在實際項目落地的時候,我么需要結(jié)合實際來進行分布式鎖方案的選擇。比如如果平臺中本身已經(jīng)有Redis
集群了,但是沒有Zookeeper
集群,那么我們就可以借助于現(xiàn)有的基礎實施來落地分布式鎖,不需要再去維護一套Zookeeper
集群。
另外根據(jù)實際的業(yè)務場景,如果并發(fā)量并不是很高,也可以通過簡單的數(shù)據(jù)庫的分布式鎖方案來實現(xiàn)。
總結(jié)
本文首先對從單機時代到分布式場景下的分布式鎖的產(chǎn)生的背景進行了分析,通過對分布式鎖的本質(zhì)問題的探究,引出了數(shù)據(jù)庫分布式鎖方案、Redis
分布式鎖方案以及Zookeeper
分布式鎖方案,并對每一種方案的優(yōu)點以及不足進行了分析,相信大家可以在落地實現(xiàn)分布式鎖的時候可以按照自身的情況選擇合適的方案。
到此這篇關(guān)于深入淺出探索Java分布式鎖原理的文章就介紹到這了,更多相關(guān)Java 分布式鎖內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解Java中Vector和ArrayList的區(qū)別
這篇文章主要為大家詳細介紹了Java中Vector和ArrayList的區(qū)別,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-10-10idea中javaweb的jsp頁面圖片加載不出來問題及解決
這篇文章主要介紹了idea中javaweb的jsp頁面圖片加載不出來問題及解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-07-07Redis Lettuce連接redis集群實現(xiàn)過程詳細講解
這篇文章主要介紹了Redis Lettuce連接redis集群實現(xiàn)過程,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習吧2023-01-01SpringBoot中的異常處理與參數(shù)校驗的方法實現(xiàn)
這篇文章主要介紹了SpringBoot中的異常處理與參數(shù)校驗的方法實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-04-04如何在 Spring Boot 中配置和使用 CSRF 保護
CSRF是一種網(wǎng)絡攻擊,它利用已認證用戶的身份來執(zhí)行未經(jīng)用戶同意的操作,Spring Boot 提供了內(nèi)置的 CSRF 保護機制,可以幫助您防止這種類型的攻擊,這篇文章主要介紹了Spring?Boot?中的?CSRF?保護配置的使用方法,需要的朋友可以參考下2023-09-09