前幾周寫了篇 利用Redis實現分佈式鎖 ,今天簡單總結下ZooKeeper實現分佈式鎖的過程。其實生產上我只用過Redis或者數據庫的方式,以前還真沒了解過ZooKeeper怎麼實現分佈式鎖。這周簡單寫了個小Demo,更堅決了我繼續使用Redis的信心了。html
在分佈式解決方案中,Zookeeper是一個分佈式協調工具。當多個JVM客戶端,同時在ZooKeeper上建立相同的一個臨時節點,由於臨時節點路徑是保證惟一,只要誰可以建立節點成功,誰就可以獲取到鎖。沒有建立成功節點,就會進行等待,當釋放鎖的時候,採用事件通知給客戶端從新獲取鎖資源。若是請求超時直接返回給客戶端超時,從新請求便可。sql
爲了更好的展示效果,我這裏設置每一個線程請求須要1s,請求超時時間爲30s。數據庫
首先咱們先寫一個測試類,模擬多線程多客戶端請求的狀況:緩存
public class ZkLockTest implements Runnable { private ZkLock zkLock = new ZkDistributedLock(); public void run() { try { if (zkLock.getLock((long)30000,null)) { System.out.println("線程:" + Thread.currentThread().getName() + ",搶購成功:" + System.currentTimeMillis()); } else { System.out.println("線程:" + Thread.currentThread().getName() + ",搶購超時失敗請重試:" + System.currentTimeMillis()); } Thread.sleep(1000); } catch (Exception e) { } finally { zkLock.unLock(); } } public static void main(String[] args) { System.out.println("zk分佈式鎖開始。。"); for (int i = 0; i < 100; i++) { new Thread(new ZkLockTest()).start(); } } }
模擬100個線程,去同時爭奪鎖。固然上述寫法 100個線程不會同時啓動,若是須要的話能夠用信號量的形式控制。網絡
其次,寫一個鎖的接口多線程
public interface ZkLock { // 獲取鎖 Boolean getLock(Long acquireTimeout,Long endTime); // 釋放鎖 void unLock(); }
這裏我定義了兩個接口,分別對應獲取鎖和釋放鎖。併發
在獲取鎖中有兩個參數,含義分別爲鎖超時時間和最終計算的超時時間,具體看下文代碼就懂了。nosql
public class ZkDistributedLock implements ZkLock { // 集羣鏈接地址 private String CONNECTION = "127.0.0.1:2181"; // zk客戶端鏈接 private ZkClient zkClient = new ZkClient(CONNECTION); // path路徑 private String lockPath = "/lock"; private CountDownLatch countDownLatch; //請求設置的超時時間:acquireTimeout 毫秒。最終超時時間endTime public Boolean getLock(Long acquireTimeout,Long endTime) { Boolean lock = false; if (endTime == null) { //等待超時時間 endTime = System.currentTimeMillis() + acquireTimeout; } if (tryLock()) { System.out.println("####獲取鎖成功######"); lock = true; } else { if (waitLock(endTime)) { if (getLock(null,endTime)) { lock = true; } } } return lock; } public void unLock() { if (zkClient != null) { System.out.println("#######釋放鎖#########"); zkClient.close(); } } private boolean tryLock() { try { zkClient.createEphemeral(lockPath); return true; } catch (Exception e) { return false; } } private Boolean waitLock(Long endTime) { // System.out.println("進入等待"); // 使用zk臨時事件監聽 IZkDataListener iZkDataListener = null; try { // 使用zk臨時事件監聽 iZkDataListener = new IZkDataListener() { public void handleDataDeleted(String path) throws Exception { if (countDownLatch != null) { countDownLatch.countDown(); } } public void handleDataChange(String arg0, Object arg1) throws Exception { } }; // 註冊事件通知 zkClient.subscribeDataChanges(lockPath, iZkDataListener); if (System.currentTimeMillis() < endTime) { if (zkClient.exists(lockPath)) { countDownLatch = new CountDownLatch(1); try { countDownLatch.await(); return true; } catch (Exception e) { } } else { return true; } } else { System.out.println("超時返回"); } } catch (Exception e) { } finally { // 監聽完畢後,移除事件通知 zkClient.unsubscribeDataChanges(lockPath, iZkDataListener); } return false; } }
這個類是我實現zk鎖的核心類,和上文原理圖中相似。首先用戶請求的時候須要獲取鎖,第一個爭奪到鎖的用戶執行相關邏輯後釋放鎖,在這個過程當中若是程序出錯斷開鏈接,由於臨時節點的緣故,節點也會自動刪除釋放鎖的。分佈式
另外就是其餘爭奪鎖失敗的用戶,我這裏設置了必定的等待時間,當在時間內原鎖釋放,仍是能夠從新去獲取鎖的。這裏要說下鎖釋放的監聽,在原生的zookeeper中,使用watcher須要每次先註冊,並且使用一次就須要註冊一次。而在zkClient中,沒有註冊watcher的必要,而是引入了listener的概念,即只要client在某一個節點中註冊了listener,只要服務端發生變化,就會通知當前註冊listener的客戶端。我這裏使用的是IZkDataListener,這個類是zkClient提供的一個接口,它能夠在當前節點數據內容或版本發生變化或者當前節點被刪除時觸發。高併發
觸發後咱們就能夠從新去爭奪鎖,當再次爭奪失敗進入等待時會再次檢測當前請求是否超時。
下面咱們來看下上述代碼的實現效果:
zk分佈式鎖開始。。
####獲取鎖成功######
線程:Thread-3,搶購成功:1544183770509
#######釋放鎖#########
####獲取鎖成功######
線程:Thread-81,搶購成功:1544183771555
#######釋放鎖#########
.........
超時返回
線程:Thread-11,搶購超時失敗請重試:1544183800677
超時返回
線程:Thread-1,搶購超時失敗請重試:1544183800681
#######釋放鎖#########
#######釋放鎖#########
####獲取鎖成功######
線程:Thread-49,搶購成功:1544183801710
超時返回
線程:Thread-25,搶購超時失敗請重試:1544183801729
超時返回
#######釋放鎖#########
#######釋放鎖#########
釋放鎖說的可能並不許確,應該說是關閉鏈接,有些線程其實是沒有獲得鎖的。
簡單嘗試了下zk實現分佈式鎖的方式,固然上述代碼若是應用到生產中確定問題仍是很多的,由於興趣點不在這,就不仔細研究了。簡單來講,相比其餘方式實現步驟更爲複雜,感受更容易出問題。
通過三種方式的應用和簡單實踐,總結實現分佈式鎖三種方式的優缺點以下
一、數據庫實現:
優勢,實現簡單只是for update的顯示加鎖。缺點,性能問題較大,並且自己系統在設計時是須要儘可能減輕數據庫的壓力的。
二、Redis實現:
優勢:通常互聯網項目都會集成,自己是nosql數據庫,緩存實現簡單,高併發應付自如,同時新版的Jedis完美解決了以往程序出錯,未設置超時時間死鎖的問題。
缺點:網絡問題可能會引發鎖刪除失敗,超時時間有必定的延遲。
三、ZooKeeper實現:
優勢:Zookeeper臨時節點先天可控的有效期設置,避免了程序引起的死鎖問題
缺點:實現過於繁雜,相比其餘兩種寫法更容易出問題,另外還須要單獨維護zk。
結論:
我我的更爲推薦Redis的實現方式,實現簡單,性能也比較好,同時引入集羣能夠提升可用性。Jedis多參的設置方式也較好的保證了有效期的控制和死鎖的問題。