Apache ZooKeeper - 使用ZK实现分布式锁(非公平锁/公平锁/共享锁 )
文章目錄
- 什么是分布式鎖
- 分布式死鎖
- 分類
- 排他鎖
- 共享鎖
- 實現
- 創(chuàng)建鎖
- 獲取鎖
- 釋放鎖
- Demo
- Jmeter配置
- 方案零 缺陷版本
- 方案一 非公平鎖方案
- 缺陷 (羊群效應)
- 方案二 公平鎖方案
- 方案三 共享鎖方案
什么是分布式鎖
什么是分布式鎖,以及分布式鎖在日常工作的使用場景。明確了這些,我們才能設計出一個安全穩(wěn)定的分布式鎖。
在日常開發(fā)中,我們最熟悉也常用的分布式鎖場景是在開發(fā)多線程的時候。為了協調本地應用上多個線程對某一資源的訪問,就要對該資源或數值變量進行加鎖,以保證在多線程環(huán)境下系統能夠正確地運行。在一臺服務器上的程序內部,線程可以通過系統進行線程之間的通信,實現加鎖等操作。而在分布式環(huán)境下,執(zhí)行事務的線程存在于不同的網絡服務器中,要想實現在分布式網絡下的線程協同操作,就要用到分布式鎖。
分布式死鎖
在單機環(huán)境下,多線程之間會產生死鎖問題。同樣,在分布式系統環(huán)境下,也會產生分布式死鎖的問題。
當死鎖發(fā)生時,系統資源會一直被某一個線程占用,從而導致其他線程無法訪問到該資源,最終使整個系統的業(yè)務處理或運行性能受到影響,嚴重的甚至可能導致服務器無法對外提供服務。
所以當我們在設計開發(fā)分布式系統的時候,要準備一些方案來面對可能會出現的死鎖問題,當問題發(fā)生時,系統會根據我們預先設計的方案,避免死鎖對整個系統的影響。常用的解決死鎖問題的方法有超時方法和死鎖檢測。
超時方法
在解決死鎖問題時,超時方法可能是最簡單的處理方式了。超時方式是在創(chuàng)建分布式線程的時候,對每個線程都設置一個超時時間。當該線程的超時時間到期后,無論該線程是否執(zhí)行完畢,都要關閉該線程并釋放該線程所占用的系統資源。之后其他線程就可以訪問該線程釋放的資源,這樣就不會造成分布式死鎖問題。但是這種設置超時時間的方法也有很多缺點,最主要的就是很難設置一個合適的超時時間。如果時間設置過短,可能造成線程未執(zhí)行完相關的處理邏輯,就因為超時時間到期就被迫關閉,最終導致程序執(zhí)行出錯。
死鎖檢測
死鎖檢測是處理死鎖問題的另一種方法,它解決了超時方法的缺陷。與超時方法相比,死鎖檢測方法主動檢測發(fā)現線程死鎖,在控制死鎖問題上更加靈活準確。你可以把死鎖檢測理解為一個運行在各個服務器系統上的線程或方法,該方法專門用來探索發(fā)現應用服務上的線程是否發(fā)生了死鎖。如果發(fā)生死鎖,就會觸發(fā)相應的預設處理方案。
分類
在介紹完分布式鎖的基本性質和潛在問題后,接下來我們就通過 ZooKeeper 來實現兩種比較常用的分布式鎖。
排他鎖
排他鎖也叫作獨占鎖,從名字上就可以看出它的實現原理。當我們給某一個數據對象設置了排他鎖后,只有具有該鎖的事務線程可以訪問該條數據對象,直到該條事務主動釋放鎖。否則,在這期間其他事務不能對該數據對象進行任何操作。
共享鎖
另一種分布式鎖的類型是共享鎖。它在性能上要優(yōu)于排他鎖,這是因為在共享鎖的實現中,只對數據對象的寫操作加鎖,而不為對象的讀操作進行加鎖。這樣既保證了數據對象的完整性,也兼顧了多事務情況下的讀取操作。可以說,共享鎖是寫入排他,而讀取操作則沒有限制。
實現
接下來就通過 ZooKeeper 來實現一個排他鎖。
創(chuàng)建鎖
首先,我們通過在 ZooKeeper 服務器上創(chuàng)建數據節(jié)點的方式來創(chuàng)建一個共享鎖。其實無論是共享鎖還是排他鎖,在鎖的實現方式上都是一樣的。唯一的區(qū)別在于,共享鎖為一個數據事務創(chuàng)建兩個數據節(jié)點,來區(qū)分是寫入操作還是讀取操作。
在 ZooKeeper 數據模型上的 Locks_shared 節(jié)點下創(chuàng)建臨時順序節(jié)點,臨時順序節(jié)點的名稱中帶有請求的操作類型分別是 R 讀取操作、W 寫入操作。
獲取鎖
當某一個事務在訪問共享數據時,首先需要獲取鎖。ZooKeeper 中的所有客戶端會在 Locks_shared 節(jié)點下創(chuàng)建一個臨時順序節(jié)點。根據對數據對象的操作類型創(chuàng)建不同的數據節(jié)點,如果是讀操作,就創(chuàng)建名稱中帶有 R 標志的順序節(jié)點,如果是寫入操作就創(chuàng)建帶有 W 標志的順序節(jié)點。
釋放鎖
事務邏輯執(zhí)行完畢后,需要對事物線程占有的共享鎖進行釋放。我們可以利用 ZooKeeper 中數據節(jié)點的性質來實現主動釋放鎖和被動釋放鎖兩種方式。
主動釋放鎖是當客戶端的邏輯執(zhí)行完畢,主動調用 delete 函數刪除ZooKeeper 服務上的數據節(jié)點。
而被動釋放鎖則利用臨時節(jié)點的性質,在客戶端因異常而退出時,ZooKeeper 服務端會直接刪除該臨時節(jié)點,即釋放該共享鎖。
這種實現方式正好和上面介紹的死鎖的兩種處理方式相對應。到目前為止,我們就利用 ZooKeeper 實現了一個比較完整的共享鎖。
如下圖所示,在這個實現邏輯中,首先通過創(chuàng)建數據臨時數據節(jié)點的方式實現獲取鎖的操作。創(chuàng)建數據節(jié)點分為兩種,分別是讀操作的數據節(jié)點和寫操作的數據節(jié)點。當鎖節(jié)點刪除時,注冊了該 Watch 監(jiān)控的其他客戶端也會收到通知,重新發(fā)起創(chuàng)建臨時節(jié)點嘗試獲取鎖。當事務邏輯執(zhí)行完成,客戶端會主動刪除該臨時節(jié)點釋放鎖。
Demo
NG 負載, 后端兩個節(jié)點
NG配置如下
Jmeter配置
方案零 缺陷版本
不加鎖的情況下,我們來壓一下
這是老板打來電話了。。。。。。
方案一 非公平鎖方案
缺陷 (羊群效應)
上實現方式在并發(fā)問題比較嚴重的情況下,性能較低,主要原因是,所有的連接都在對同一個節(jié)點進行監(jiān)聽,當服務器檢測到刪除事件時,要通知所有的連接,所有的連接同時收到事件,再次并發(fā)競爭,這就是羊群效應。
如何避免呢,我們看下面這種方式
方案二 公平鎖方案
上述方案是一個公平鎖的實現,通過zk提供的臨時順序節(jié)點,可以避免同時多個節(jié)點的并發(fā)競爭鎖,緩解了服務端壓力,避免羊群效應。
代碼實現,Curator框架提供了InterProcessMutex
https://curator.apache.org/getting-started.html
@PostMapping("/stock/deductWithLock")public Object deductWithLock(Integer id) throws Exception {InterProcessMutex interProcessMutex = new InterProcessMutex(curatorFramework, "/product_" + id);try {interProcessMutex.acquire();orderService.reduceStock(id);} catch (Exception e) {if (e instanceof RuntimeException) {throw e;}}finally {interProcessMutex.release();}return "visit:" + port;}方案三 共享鎖方案
實現:InterProcessReadWriteLock
https://curator.apache.org/curator-recipes/shared-reentrant-read-write-lock.html
用法都很簡單,主要是搞清楚實現原理
行了,到這兒吧先
總結
以上是生活随笔為你收集整理的Apache ZooKeeper - 使用ZK实现分布式锁(非公平锁/公平锁/共享锁 )的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Apache ZooKeeper -
- 下一篇: 小工匠聊架构 - 分布式缓存技术_缓存设