ZooKeeper分散式鎖的實現。

在分散式的情況下,sychornized 和 Lock 已經不能滿足我們的要求了,那麼就需要使用第三方的鎖了,這裡我們就使用 ZooKeeper 來實現一個分散式鎖。

一、分散式鎖方案比較

方案 實現思路 優點 缺點
利用 MySQL 的實現方案 利用資料庫自身提供的鎖機制實現,要求資料庫支援行級鎖 實現簡單 效能差,無法適應高併發場景;容易出現死鎖的情況;無法優雅的實現阻塞式鎖
利用 Redis 的實現方案 使用 Setnx 和 lua 指令碼機制實現,保證對快取操作序列的原子性 效能好 實現相對複雜,有可能出現死鎖;無法優雅的實現阻塞式鎖
利用 ZooKeeper 的實現方案 基於 ZooKeeper 節點特性及 watch 機制實現 效能好,穩定可靠性高,能較好地實現阻塞式鎖 實現相對複雜

二、ZooKeeper實現分散式鎖

這裡使用 ZooKeeper 來實現分散式鎖,以50個併發請求來獲取訂單編號為例,描述兩種方案,第一種為基礎實現,第二種在第一種基礎上進行了優化。

1. 方案一

流程描述:

具體程式碼:

OrderNumGenerator:

/**
* @Author SunnyBear
* @Description 生成隨機訂單號
*/
public class OrderNumGenerator { private static long count = 0; /**
* 使用日期加數值拼接成訂單號
*/
public String getOrderNumber() throws Exception {
String date = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now());
String number = new DecimalFormat("000000").format(count++);
return date + number;
}
}

Lock:

/**
* @Author SunnyBear
* @Description 自定義鎖介面
*/
public interface Lock { /**
* 獲取鎖
*/
public void getLock(); /**
* 釋放鎖
*/
public void unLock();
}

AbstractLock:

/**
* @Author SunnyBear
* @Description 定義一個模板,具體的方法由子類來實現
*/
public abstract class AbstractLock implements Lock { /**
* 獲取鎖
*/
@Override
public void getLock() { if (tryLock()) {
System.out.println("--------獲取到了自定義Lock鎖的資源--------");
} else {
// 沒拿到鎖則阻塞,等待拿鎖
waitLock();
getLock();
} } /**
* 嘗試獲取鎖,如果拿到了鎖返回true,沒有拿到則返回false
*/
public abstract boolean tryLock(); /**
* 阻塞,等待獲取鎖
*/
public abstract void waitLock();
}

ZooKeeperAbstractLock:

/**
* @Author SunnyBear
* @Description 定義需要的服務連線
*/
public abstract class ZooKeeperAbstractLock extends AbstractLock { private static final String SERVER_ADDR = "192.168.182.130:2181,192.168.182.131:2181,192.168.182.132:2181"; protected ZkClient zkClient = new ZkClient(SERVER_ADDR); protected static final String PATH = "/lock";
}

ZooKeeperDistrbuteLock:

/**
* @Author SunnyBear
* @Description 真正實現鎖的細節
*/
public class ZooKeeperDistrbuteLock extends ZooKeeperAbstractLock {
private CountDownLatch countDownLatch = null; /**
* 嘗試拿鎖
*/
@Override
public boolean tryLock() {
try {
// 建立臨時節點
zkClient.createEphemeral(PATH);
return true;
} catch (Exception e) {
// 建立失敗報異常
return false;
}
} /**
* 阻塞,等待獲取鎖
*/
@Override
public void waitLock() {
// 建立監聽
IZkDataListener iZkDataListener = new IZkDataListener() {
@Override
public void handleDataChange(String s, Object o) throws Exception { } @Override
public void handleDataDeleted(String s) throws Exception {
// 釋放鎖,刪除節點時喚醒等待的執行緒
if (countDownLatch != null) {
countDownLatch.countDown();
}
}
}; // 註冊監聽
zkClient.subscribeDataChanges(PATH, iZkDataListener); // 節點存在時,等待節點刪除喚醒
if (zkClient.exists(PATH)) {
countDownLatch = new CountDownLatch(1);
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
} // 刪除監聽
zkClient.unsubscribeDataChanges(PATH, iZkDataListener);
} /**
* 釋放鎖
*/
@Override
public void unLock() {
if (zkClient != null) {
System.out.println("釋放鎖資源");
zkClient.delete(PATH);
zkClient.close();
}
}
}

測試效果:使用50個執行緒來併發測試ZooKeeper實現的分散式鎖

/**
* @Author SunnyBear
* @Description 使用50個執行緒來併發測試ZooKeeper實現的分散式鎖
*/
public class OrderService { private static class OrderNumGeneratorService implements Runnable { private OrderNumGenerator orderNumGenerator = new OrderNumGenerator();;
private Lock lock = new ZooKeeperDistrbuteLock(); @Override
public void run() {
lock.getLock();
try {
System.out.println(Thread.currentThread().getName() + ", 生成訂單編號:" + orderNumGenerator.getOrderNumber());
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unLock();
}
}
} public static void main(String[] args) {
System.out.println("----------生成唯一訂單號----------");
for (int i = 0; i < 50; i++) {
new Thread(new OrderNumGeneratorService()).start();
}
}
}

2. 方案二

方案二在方案一的基礎上進行優化,避免產生“羊群效應”,方案一一旦臨時節點刪除,釋放鎖,那麼其他在監聽這個節點變化的執行緒,就會去競爭鎖,同時訪問 ZooKeeper,那麼怎麼更好的避免各執行緒的競爭現象呢,就是使用臨時順序節點,臨時順序節點排序,每個臨時順序節點只監聽它本身的前一個節點變化。

流程描述:

具體程式碼

具體只需要將方案一中的 ZooKeeperDistrbuteLock 改變,增加一個 ZooKeeperDistrbuteLock2,測試程式碼中使用 ZooKeeperDistrbuteLock2 即可測試,其他程式碼都不需要改變。

/**
* @Author SunnyBear
* @Description 真正實現鎖的細節
*/
public class ZooKeeperDistrbuteLock2 extends ZooKeeperAbstractLock { private CountDownLatch countDownLatch = null;
/**
* 當前請求節點的前一個節點
*/
private String beforePath;
/**
* 當前請求的節點
*/
private String currentPath; public ZooKeeperDistrbuteLock2() {
if (!zkClient.exists(PATH)) {
// 建立持久節點,儲存臨時順序節點
zkClient.createPersistent(PATH);
}
} @Override
public boolean tryLock() {
// 如果currentPath為空則為第一次嘗試拿鎖,第一次拿鎖賦值currentPath
if (currentPath == null || currentPath.length() == 0) {
// 在指定的持久節點下建立臨時順序節點
currentPath = zkClient.createEphemeralSequential(PATH + "/", "lock");
}
// 獲取所有臨時節點並排序,例如:000044
List<String> childrenList = zkClient.getChildren(PATH);
Collections.sort(childrenList); if (currentPath.equals(PATH + "/" + childrenList.get(0))) {
// 如果當前節點在所有節點中排名第一則獲取鎖成功
return true;
} else {
int wz = Collections.binarySearch(childrenList, currentPath.substring(6));
beforePath = PATH + "/" + childrenList.get(wz - 1);
}
return false;
} @Override
public void waitLock() {
// 建立監聽
IZkDataListener iZkDataListener = new IZkDataListener() {
@Override
public void handleDataChange(String s, Object o) throws Exception { } @Override
public void handleDataDeleted(String s) throws Exception {
// 釋放鎖,刪除節點時喚醒等待的執行緒
if (countDownLatch != null) {
countDownLatch.countDown();
}
}
}; // 註冊監聽,這裡是給排在當前節點前面的節點增加(刪除資料的)監聽,本質是啟動另外一個執行緒去監聽前置節點
zkClient.subscribeDataChanges(beforePath, iZkDataListener); // 前置節點存在時,等待前置節點刪除喚醒
if (zkClient.exists(beforePath)) {
countDownLatch = new CountDownLatch(1);
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
} // 刪除對前置節點的監聽
zkClient.unsubscribeDataChanges(beforePath, iZkDataListener);
} /**
* 釋放鎖
*/
@Override
public void unLock() {
if (zkClient != null) {
System.out.println("釋放鎖資源");
zkClient.delete(currentPath);
zkClient.close();
}
}
}

都讀到這裡了,來個 點贊、評論、關注、收藏 吧!

文章作者:IT王小二

首發地址:https://www.itwxe.com/posts/6d9ff3d6/

版權宣告:文章內容遵循 署名-非商業性使用-禁止演繹 4.0 國際 進行許可,轉載請在文章頁面明顯位置給出作者與原文連結。