當前位置:編程學習大全網 - 編程語言 - 高並發沒鎖可不行,三種分布式鎖詳解

高並發沒鎖可不行,三種分布式鎖詳解

Java中的鎖主要包括synchronized鎖和JUC包中的鎖,這些鎖都是針對單個JVM實例上的鎖,對於分布式環境如果我們需要加鎖就顯得無能為力。在單個JVM實例上,鎖的競爭者通常是壹些不同的線程,而在分布式環境中,鎖的競爭者通常是壹些不同的線程或者進程。如何實現在分布式環境中對壹個對象進行加鎖呢?答案就是分布式鎖。

目前分布式鎖的實現方案主要包括三種:

基於數據庫實現分布式鎖主要是利用數據庫的唯壹索引來實現,唯壹索引天然具有排他性,這剛好符合我們對鎖的要求:同壹時刻只能允許壹個競爭者獲取鎖。加鎖時我們在數據庫中插入壹條鎖記錄,利用業務id進行防重。當第壹個競爭者加鎖成功後,第二個競爭者再來加鎖就會拋出唯壹索引沖突,如果拋出這個異常,我們就判定當前競爭者加鎖失敗。防重業務id需要我們自己來定義,例如我們的鎖對象是壹個方法,則我們的業務防重id就是這個方法的名字,如果鎖定的對象是壹個類,則業務防重id就是這個類名。

基於緩存實現分布式鎖:理論上來說使用緩存來實現分布式鎖的效率最高,加鎖速度最快,因為Redis幾乎都是純內存操作,而基於數據庫的方案和基於Zookeeper的方案都會涉及到磁盤文件IO,效率相對低下。壹般使用Redis來實現分布式鎖都是利用Redis的 SETNX key value 這個命令,只有當key不存在時才會執行成功,如果key已經存在則命令執行失敗。

基於Zookeeper:Zookeeper壹般用作配置中心,其實現分布式鎖的原理和Redis類似,我們在Zookeeper中創建瞬時節點,利用節點不能重復創建的特性來保證排他性。

在實現分布式鎖的時候我們需要考慮壹些問題,例如:分布式鎖是否可重入,分布式鎖的釋放時機,分布式鎖服務端是否有單點問題等。

上面已經分析了基於數據庫實現分布式鎖的基本原理:通過唯壹索引保持排他性,加鎖時插入壹條記錄,解鎖是刪除這條記錄。下面我們就簡要實現壹下基於數據庫的分布式鎖。

id字段是數據庫的自增id,unique_mutex字段就是我們的防重id,也就是加鎖的對象,此對象唯壹。在這張表上我們加了壹個唯壹索引,保證unique_mutex唯壹性。holder_id代表競爭到鎖的持有者id。

如果當前sql執行成功代表加鎖成功,如果拋出唯壹索引異常(DuplicatedKeyException)則代表加鎖失敗,當前鎖已經被其他競爭者獲取。

解鎖很簡單,直接刪除此條記錄即可。

是否可重入 :就以上的方案來說,我們實現的分布式鎖是不可重入的,即是是同壹個競爭者,在獲取鎖後未釋放鎖之前再來加鎖,壹樣會加鎖失敗,因此是不可重入的。解決不可重入問題也很簡單:加鎖時判斷記錄中是否存在unique_mutex的記錄,如果存在且holder_id和當前競爭者id相同,則加鎖成功。這樣就可以解決不可重入問題。

鎖釋放時機 :設想如果壹個競爭者獲取鎖時候,進程掛了,此時distributed_lock表中的這條記錄就會壹直存在,其他競爭者無法加鎖。為了解決這個問題,每次加鎖之前我們先判斷已經存在的記錄的創建時間和當前系統時間之間的差是否已經超過超時時間,如果已經超過則先刪除這條記錄,再插入新的記錄。另外在解鎖時,必須是鎖的持有者來解鎖,其他競爭者無法解鎖。這點可以通過holder_id字段來判定。

數據庫單點問題 :單個數據庫容易產生單點問題:如果數據庫掛了,我們的鎖服務就掛了。對於這個問題,可以考慮實現數據庫的高可用方案,例如MySQL的MHA高可用解決方案。

使用Jedis來和Redis通信。

可以看到,我們加鎖就壹行代碼:

jedis.set(String key, String value, String nxxx, String expx, int time);

這個set()方法壹***五個形參:

第壹個為key,我們使用key來當鎖,因為key是唯壹的。

第二個為value,這裏寫的是鎖競爭者的id,在解鎖時,我們需要判斷當前解鎖的競爭者id是否為鎖持有者。

第三個為nxxx,這個參數我們填的是NX,意思是SET IF NOT EXIST,即當key不存在時,我們進行set操作;若key已經存在,則不做任何操作。

第四個為expx,這個參數我們傳的是PX,意思是我們要給這個key加壹個過期時間的設置,具體時間由第五個參數決定;

第五個參數為time,與第四個參數相呼應,代表key的過期時間。

總的來說,執行上面的set()方法就只會導致兩種結果:1.當前沒有鎖(key不存在),那麽久進行加鎖操作,並對鎖設置壹個有效期,同時value表示加鎖的客戶端。2.已經有鎖存在,不做任何操作。

上述解鎖請求中, SET_IF_NOT_EXIST (不存在則執行)保證了加鎖請求的排他性,緩存超時機制保證了即使壹個競爭者加鎖之後掛了,也不會產生死鎖問題:超時之後其他競爭者依然可以獲取鎖。通過設置value為競爭者的id,保證了只有鎖的持有者才能來解鎖,否則任何競爭者都能解鎖,那豈不是亂套了。

解鎖的步驟:

註意到這裏解鎖其實是分為2個步驟,涉及到解鎖操作的壹個原子性操作問題。這也是為什麽我們解鎖的時候用Lua腳本來實現,因為Lua腳本可以保證操作的原子性。那麽這裏為什麽需要保證這兩個步驟的操作是原子操作呢?

設想:假設當前鎖的持有者是競爭者1,競爭者1來解鎖,成功執行第1步,判斷自己就是鎖持有者,這是還未執行第2步。這是鎖過期了,然後競爭者2對這個key進行了加鎖。加鎖完成後,競爭者1又來執行第2步,此時錯誤產生了:競爭者1解鎖了不屬於自己持有的鎖。可能會有人問為什麽競爭者1執行完第1步之後突然停止了呢?這個問題其實很好回答,例如競爭者1所在的JVM發生了GC停頓,導致競爭者1的線程停頓。這樣的情況發生的概率很低,但是請記住即使只有萬分之壹的概率,在線上環境中完全可能發生。因此必須保證這兩個步驟的操作是原子操作。

是否可重入 :以上實現的鎖是不可重入的,如果需要實現可重入,在 SET_IF_NOT_EXIST 之後,再判斷key對應的value是否為當前競爭者id,如果是返回加鎖成功,否則失敗。

鎖釋放時機 :加鎖時我們設置了key的超時,當超時後,如果還未解鎖,則自動刪除key達到解鎖的目的。如果壹個競爭者獲取鎖之後掛了,我們的鎖服務最多也就在超時時間的這段時間之內不可用。

Redis單點問題 :如果需要保證鎖服務的高可用,可以對Redis做高可用方案:Redis集群+主從切換。目前都有比較成熟的解決方案。

利用Zookeeper創建臨時有序節點來實現分布式鎖:

其基本思想類似於AQS中的等待隊列,將請求排隊處理。其流程圖如下:

解決不可重入 :客戶端加鎖時將主機和線程信息寫入鎖中,下壹次再來加鎖時直接和序列最小的節點對比,如果相同,則加鎖成功,鎖重入。

鎖釋放時機 :由於我們創建的節點是順序臨時節點,當客戶端獲取鎖成功之後突然session會話斷開,ZK會自動刪除這個臨時節點。

單點問題 :ZK是集群部署的,主要壹半以上的機器存活,就可以保證服務可用性。

Zookeeper第三方客戶端curator中已經實現了基於Zookeeper的分布式鎖。利用curator加鎖和解鎖的代碼如下:

  • 上一篇:vue3.0什麽時候發布的
  • 下一篇:數學建模論文,求答案
  • copyright 2024編程學習大全網