1. 程式人生 > 程式設計 >ZooKeeper分散式專題(七)-- 使用zookeeper實現分散式鎖

ZooKeeper分散式專題(七)-- 使用zookeeper實現分散式鎖

ZooKeeper分散式專題與Dubbo微服務入門

zookeeper實現分散式鎖

什麼多執行緒

多執行緒為了能夠提高應用程式的執行效率,在一個程式中有多條不同的執行路徑,同時並行執行,互不影響。

這裡關於執行緒的介紹就不多闡述,想了解更多關於執行緒的介紹請移步 github.com/haoxiaoyong…

下面我們只針對分散式環境下實現分散式鎖介紹;

什麼是java記憶體模型

共享記憶體模型指的就是Java記憶體模型(簡稱JMM),JMM決定一個執行緒對共享變數的寫入時,能對另一個執行緒可見。從抽象的角度來看,JMM定義了執行緒和主記憶體之間的抽象關係:執行緒之間的共享變數儲存在主記憶體(main memory)中,每個執行緒都有一個私有的本地記憶體(local memory),本地記憶體中儲存了該執行緒以讀/寫共享變數的副本。本地記憶體是JMM的一個抽象概念,並不真實存在。它涵蓋了快取,寫緩衝區,暫存器以及其他的硬體和編譯器優化

image.png

從上圖來看,執行緒A與執行緒B之間如要通訊的話,必須要經歷下面2個步驟:

  1. 首先,執行緒A把本地記憶體A中更新過的共享變數重新整理到主記憶體中去。

  2. 然後,執行緒B到主記憶體中去讀取執行緒A之前已更新過的共享變數。

image.png

如上圖所示,本地記憶體A和B有主記憶體中共享變數x的副本。假設初始時,這三個記憶體中的x值都為0。執行緒A在執行時,把更新後的x值(假設值為1)臨時存放在自己的本地記憶體A中。當執行緒A和執行緒B需要通訊時,執行緒A首先會把自己本地記憶體中修改後的x值重新整理到主記憶體中,此時主記憶體中的x值變為了1。隨後,執行緒B到主記憶體中去讀取執行緒A更新後的x值,此時執行緒B的本地記憶體的x值也變為了1。 從整體來看,這兩個步驟實質上是執行緒A在向執行緒B傳送訊息,而且這個通訊過程必須要經過主記憶體。JMM通過控制主記憶體與每個執行緒的本地記憶體之間的互動,來為java程式提供記憶體可見性保證。

總結:什麼是Java記憶體模型:java記憶體模型簡稱jmm,定義了一個執行緒對另一個執行緒可見。共享變數存放在主記憶體中,每個執行緒都有自己的本地記憶體,當多個執行緒同時訪問一個資料的時候,可能本地記憶體沒有及時重新整理到主記憶體,所以就會發生執行緒安全問題。

傳統方式生成訂單號ID

生成訂單類

public class OrderNumGenerator {

    //全域性訂單id;
    public static int count = 0;

    public String getNumber() {

        try {
            //TimeUnit.SECONDS.sleep(2);
Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } SimpleDateFormat simpt = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss"); return simpt.format(new Date()) + "-" + ++count; } } 複製程式碼

使用多執行緒情況模擬生成訂單號

public class OrderService implements Runnable {

    private OrderNumGenerator orderNumGenerator = new OrderNumGenerator();

    public void run() {
        getNumber();
    }

    public void getNumber() {
        String number = orderNumGenerator.getNumber();
        System.out.println(Thread.currentThread().getName() + ",生成訂單ID:" + number);
    }

    public static void main(String[] args) {
        System.out.println("####生成唯一訂單號###");
        for (int i = 0; i < 100; i++) {
            new Thread(new OrderService()).start();
        }

    }
}
複製程式碼

這時候會出現執行緒安全問題;

image.png

下面解決這種執行緒安全問題的方式有很多

例如:使用synchronized或者lock鎖

這裡對synchronized就不做過的說明瞭。想了解更多關於synchronized的語義及使用請移步 github.com/haoxiaoyong…

使用lock鎖解決執行緒安全問題:

生成訂單類

public class OrderNumGenerator {

    //全域性訂單id;
    public static int count = 0;

    public String getNumber() {

        try {
            //TimeUnit.SECONDS.sleep(2);
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        SimpleDateFormat simpt = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");
        return simpt.format(new Date()) + "-" + ++count;
    }
}

複製程式碼

沒有做任何的改變;

使用多執行緒情況模擬生成訂單號(lock鎖):

public class OrderService implements Runnable {

    private OrderNumGenerator orderNumGenerator = new OrderNumGenerator();

    // 使用lock鎖
    private java.util.concurrent.locks.Lock lock = new ReentrantLock();

    public void run() {
        getNumber();
    }

    public void getNumber() {
        try {
            lock.lock();
            String number = orderNumGenerator.getNumber();
            System.out.println(Thread.currentThread().getName() + ",生成訂單ID:" + number);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        System.out.println("####生成唯一訂單號###");
        OrderService orderService = new OrderService();
        for (int i = 0; i < 100; i++) {
            new Thread(orderService).start();
        }

    }
}

複製程式碼

對比和之前和那些改變?

lock.lock();上鎖,

lock.unlock();釋放鎖

同時我們要注意main方法中的OrderService物件;這裡只例項化一次;

image.png

很完美的列印到100;

下面介紹在分散式環境下生成訂單ID;

在分散式(叢集)環境下,每臺JVM不能實現同步,在分散式場景下使用時間戳生成訂單號可能會重複

使用分散式鎖生成訂單號技術

1.使用資料庫實現分散式鎖 缺點:效能差、執行緒出現異常時,容易出現死鎖 2.使用redis實現分散式鎖 缺點:鎖的失效時間難控制、容易產生死鎖、非阻塞式、不可重入 3.使用zookeeper實現分散式鎖 實現相對簡單、可靠性強、使用臨時節點,失效時間容易控制

什麼是分散式鎖

分散式鎖一般用在分散式系統或者多個應用中,用來控制同一任務是否執行或者任務的執行順序。在專案中,部署了多個tomcat應用,在執行定時任務時就會遇到同一任務可能執行多次的情況,我們可以藉助分散式鎖,保證在同一時間只有一個tomcat應用執行了定時任務

使用Zookeeper實現分散式鎖

Zookeeper實現分散式鎖原理

使用zookeeper建立臨時序列節點來實現分散式鎖,適用於順序執行的程式,大體思路就是建立臨時序列節點,找出最小的序列節點,獲取分散式鎖,程式執行完成之後此序列節點消失,通過watch來監控節點的變化,從剩下的節點的找到最小的序列節點,獲取分散式鎖,執行相應處理,依次類推……

新增依賴

<dependency>
   <groupId>com.101tec</groupId>
   <artifactId>zkclient</artifactId>
   <version>0.10</version>
</dependency>
複製程式碼

建立Lock介面

public interface Lock {

    //獲取到鎖的資源
    void getLock();
    // 釋放鎖
    void unLock();

}
複製程式碼

建立ZookeeperAbstractLock抽象類

public abstract class ZookeeperAbstractLock implements Lock {

    // zk連線地址
    private static final String CONNECTSTRING = "127.0.0.1:2181";

    // 建立zk連線
    protected ZkClient zkClient = new ZkClient(CONNECTSTRING);

    protected static final String PATH = "/lock";

    public void getLock(){
        if(tryLock()){
            System.out.println("##獲取lock鎖的資源####");
        }else {
            //等待
            waitLock();
            //重新獲取資源
            getLock();
        }
    }

    //獲取鎖資源
    abstract boolean tryLock();

    //等待
    abstract void waitLock();

    public void unLock() {
        if (zkClient != null) {
            zkClient.close();
            System.out.println("釋放鎖資源...");
        }
    }
}
複製程式碼

ZookeeperDistrbuteLock類

public class ZookeeperDistrbuteLock extends ZookeeperAbstractLock {

    private CountDownLatch countDownLatch = null;

    boolean tryLock() {

        try {
            zkClient.createEphemeral(PATH);
            return true;
        } catch (Exception e) {
//       e.printStackTrace();
            return false;
        }
    }

    void waitLock() {
        IZkDataListener izkDataListener = new IZkDataListener() {

            public void handleDataDeleted(String path) throws Exception {
                // 喚醒被等待的執行緒
                if (countDownLatch != null) {
                    countDownLatch.countDown();
                }
            }

            public void handleDataChange(String path,Object data) throws Exception {

            }
        };
        // 註冊事件
        zkClient.subscribeDataChanges(PATH,izkDataListener);
        if (zkClient.exists(PATH)) {
            countDownLatch = new CountDownLatch(1);
            try {
                countDownLatch.await();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        // 刪除監聽
        zkClient.unsubscribeDataChanges(PATH,izkDataListener);
    }

}
複製程式碼

使用Zookeeper鎖執行效果

public class OrderService implements Runnable {
    
   private OrderNumGenerator orderNumGenerator = new OrderNumGenerator();
   // 使用lock鎖
   // private java.util.concurrent.locks.Lock lock = new ReentrantLock();
   private Lock lock = new ZookeeperDistrbuteLock();
   public void run() {
      getNumber();
   }
   public void getNumber() {
      try {
         lock.getLock();
         String number = orderNumGenerator.getNumber();
         System.out.println(Thread.currentThread().getName() + ",生成訂單ID:" + number);
      } catch (Exception e) {
         e.printStackTrace();
      } finally {
         lock.unLock();
      }
   }
   public static void main(String[] args) {
      System.out.println("####生成唯一訂單號###");
//    OrderService orderService = new OrderService();
      for (int i = 0; i < 100; i++) {
         new Thread( new OrderService()).start();
      }
   }
}
複製程式碼

執行main方法:

##獲取lock鎖的資源####
Thread-1,生成訂單ID:2019-08-19-22-32-50-1
釋放鎖資源...
##獲取lock鎖的資源####
Thread-3,生成訂單ID:2019-08-19-22-32-59-2
釋放鎖資源...
##獲取lock鎖的資源####
Thread-5,生成訂單ID:2019-08-19-22-33-08-3
釋放鎖資源...
##獲取lock鎖的資源####
Thread-7,生成訂單ID:2019-08-19-22-33-17-4
釋放鎖資源...
##獲取lock鎖的資源####
Thread-9,生成訂單ID:2019-08-19-22-33-26-5
釋放鎖資源...
##獲取lock鎖的資源####
Thread-11,生成訂單ID:2019-08-19-22-33-35-6
釋放鎖資源...
##獲取lock鎖的資源####
Thread-13,生成訂單ID:2019-08-19-22-33-44-7
釋放鎖資源...
##獲取lock鎖的資源####
Thread-15,生成訂單ID:2019-08-19-22-33-53-8
釋放鎖資源...
##獲取lock鎖的資源####
Thread-17,生成訂單ID:2019-08-19-22-34-02-9
釋放鎖資源...
##獲取lock鎖的資源####
Thread-19,生成訂單ID:2019-08-19-22-34-11-10
釋放鎖資源...
##獲取lock鎖的資源####
·····
複製程式碼