溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

如何使用ZooKeeper實(shí)現(xiàn)Java跨JVM的分布式鎖

發(fā)布時(shí)間:2021-08-12 14:04:37 來源:億速云 閱讀:146 作者:chen 欄目:云計(jì)算

這篇文章主要介紹“如何使用ZooKeeper實(shí)現(xiàn)Java跨JVM的分布式鎖 ”,在日常操作中,相信很多人在如何使用ZooKeeper實(shí)現(xiàn)Java跨JVM的分布式鎖 問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對(duì)大家解答”如何使用ZooKeeper實(shí)現(xiàn)Java跨JVM的分布式鎖 ”的疑惑有所幫助!接下來,請(qǐng)跟著小編一起來學(xué)習(xí)吧!

一、使用ZooKeeper實(shí)現(xiàn)Java跨JVM的分布式鎖

Zookeeper版本為 Release 3.4.8(stable)

Curator版本為2.9.1

<dependency>
		    <groupId>org.apache.zookeeper</groupId>
		    <artifactId>zookeeper</artifactId>
		    <version>3.4.8</version>
		</dependency>
		
		<dependency>
		    <groupId>org.apache.curator</groupId>
		    <artifactId>curator-recipes</artifactId>
		    <version>2.9.1</version>
		</dependency>
		
		<dependency>
		    <groupId>org.apache.curator</groupId>
		    <artifactId>curator-client</artifactId>
		    <version>2.9.1</version>
		</dependency>

鎖原理:

1、首先要?jiǎng)?chuàng)建一個(gè)鎖的根節(jié)點(diǎn),比如/mylock。

2、想要獲取鎖的客戶端在鎖的根節(jié)點(diǎn)下面創(chuàng)建znode,作為/mylock的子節(jié)點(diǎn),節(jié)點(diǎn)的類型要選擇CreateMode.PERSISTENT_SEQUENTIAL,節(jié)點(diǎn)的名字最好用uuid(至于為什么用uuid我后面會(huì)講,先說一下~如果不這么做在某種情況下會(huì)發(fā)生死鎖,這一點(diǎn)我看了很多國內(nèi)朋友自己的實(shí)現(xiàn),都沒有考慮到這一層,這也是我為什么不建議大家自己去封裝這種鎖,因?yàn)樗_實(shí)很復(fù)雜),假設(shè)目前同時(shí)有3個(gè)客戶端想要獲得鎖,那么/mylock下的目錄應(yīng)該是這個(gè)樣子的。

xxx-lock-0000000001,xxx-lock-0000000002,xxx-lock-0000000003

xxx為uuid , 0000000001,0000000002,0000000003 是zook服務(wù)端自動(dòng)生成的自增數(shù)字。

3、當(dāng)前客戶端通過getChildren(/mylock)獲取所有子節(jié)點(diǎn)列表并根據(jù)自增數(shù)字排序,然后判斷一下自己創(chuàng)建的節(jié)點(diǎn)的順序是不是在列表當(dāng)中最小的,如果是 那么獲取到鎖,如果不是,那么獲取自己的前一個(gè)節(jié)點(diǎn),并設(shè)置監(jiān)聽這個(gè)節(jié)點(diǎn)的變化,當(dāng)節(jié)點(diǎn)變化時(shí)重新執(zhí)行步驟3 直到自己是編號(hào)最小的一個(gè)為止。

舉例:假設(shè)當(dāng)前客戶端創(chuàng)建的節(jié)點(diǎn)是0000000002,因?yàn)樗木幪?hào)不是最小的,所以獲取不到鎖,那么它就找到它前面的一個(gè)節(jié)點(diǎn)0000000001 并對(duì)它設(shè)置監(jiān)聽。

4、釋放鎖,當(dāng)前獲得鎖的客戶端在操作完成后刪除自己創(chuàng)建的節(jié)點(diǎn),這樣會(huì)激發(fā)zook的事件給其它客戶端知道,這樣其它客戶端會(huì)重新執(zhí)行(步驟3)。

舉例:加入客戶端0000000001獲取到鎖,然后客戶端0000000002加入進(jìn)來獲取鎖,發(fā)現(xiàn)自己不是編號(hào)最小的,那么它會(huì)監(jiān)聽它前面節(jié)點(diǎn)的事件(0000000001的事件)然后執(zhí)行步驟(3),當(dāng)客戶端0000000001操作完成后刪除自己的節(jié)點(diǎn),這時(shí)zook服務(wù)端會(huì)發(fā)送事件,這時(shí)客戶端0000000002會(huì)接收到該事件,然后重復(fù)步驟3直到獲取到鎖)

上面的步驟實(shí)現(xiàn)了一個(gè)有序鎖,也就是先進(jìn)入等待鎖的客戶端在鎖可用時(shí)先獲得鎖。

如果想要實(shí)現(xiàn)一個(gè)隨機(jī)鎖,那么只需要把PERSISTENT_SEQUENTIAL換成一個(gè)隨機(jī)數(shù)即可。

簡單示例:

package com.framework.code.demo.zook;

import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;

public class CuratorDemo {

	public static void main(String[] args) throws Exception {
		
		//操作失敗重試機(jī)制 1000毫秒間隔 重試3次
		RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
		//創(chuàng)建Curator客戶端
		CuratorFramework client = CuratorFrameworkFactory.newClient("192.168.1.18:2181", retryPolicy);
		//開始
		client.start();
		
		/**
		 * 這個(gè)類是線程安全的,一個(gè)JVM創(chuàng)建一個(gè)就好
		 * mylock 為鎖的根目錄,我們可以針對(duì)不同的業(yè)務(wù)創(chuàng)建不同的根目錄
		 */
		final InterProcessMutex lock = new InterProcessMutex(client, "/mylock");
	    try {
	    	//阻塞方法,獲取不到鎖線程會(huì)掛起。
	    	lock.acquire();
	    	System.out.println("已經(jīng)獲取到鎖");
	    	 Thread.sleep(10000);
	    } catch (Exception e) {
			e.printStackTrace();
		}
	    finally{
	    	//釋放鎖,必須要放到finally里面,已確保上面方法出現(xiàn)異常時(shí)也能夠釋放鎖。
	    	lock.release();
	    }
		
	    Thread.sleep(10000);
	    
		client.close();
	}

}


上面代碼再獲取鎖的地方暫停了10秒鐘,我們使用zook的客戶端去查看目錄的創(chuàng)建情況,由于我前面已經(jīng)做了幾次測試,所以序號(hào)是從12開始的。

如何使用ZooKeeper實(shí)現(xiàn)Java跨JVM的分布式鎖

模擬多個(gè)客戶端(也可以認(rèn)為是多個(gè)JVM):

現(xiàn)在把上面的代碼改造一下放入到線程中去執(zhí)行,模擬多個(gè)客戶端測試。

public class CuratorDemo {

	public static void main(String[] args) throws Exception {
		for (int i = 0; i < 10; i++) {
			//啟動(dòng)10個(gè)線程模擬多個(gè)客戶端
			Jvmlock jl = new Jvmlock(i);
			new Thread(jl).start();
			//這里加上300毫秒是為了讓線程按順序啟動(dòng),不然有可能4號(hào)線程比3號(hào)線程先啟動(dòng)了,這樣測試就不準(zhǔn)了。
			Thread.sleep(300);
		}
	}
	
	public static class Jvmlock implements Runnable{
		
		private int num;
		public Jvmlock(int num) {
			this.num = num;
		}
		
		@Override
		public void run() {
			RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000,3);
			CuratorFramework client = CuratorFrameworkFactory
					.newClient("192.168.142.128:2181", retryPolicy);
			client.start();
			
			InterProcessMutex lock = new InterProcessMutex(client,
					"/mylock");
			try {
				System.out.println("我是第" + num + "號(hào)線程,我開始獲取鎖");
				lock.acquire();
				System.out.println("我是第" + num + "號(hào)線程,我已經(jīng)獲取鎖");
				Thread.sleep(10000);
			} catch (Exception e) {
				e.printStackTrace();
			} finally {
				try {
					lock.release();
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
			client.close();
		}
	}

}


通過客戶端軟件我們可以看到10個(gè)申請(qǐng)鎖的節(jié)點(diǎn)已經(jīng)被創(chuàng)建出來了。

如何使用ZooKeeper實(shí)現(xiàn)Java跨JVM的分布式鎖

看一下打印結(jié)果,先申請(qǐng)獲取鎖的線程在鎖可用時(shí)最先獲取到鎖,因?yàn)樗麄兩暾?qǐng)鎖時(shí)創(chuàng)建節(jié)點(diǎn)的順序號(hào)是遞增的,先申請(qǐng)鎖的客戶端創(chuàng)建的節(jié)點(diǎn)編號(hào)最小,所以先獲取到鎖

我是第0號(hào)線程,我開始獲取鎖
我是第0號(hào)線程,我已經(jīng)獲取鎖
我是第1號(hào)線程,我開始獲取鎖
我是第2號(hào)線程,我開始獲取鎖
我是第3號(hào)線程,我開始獲取鎖
我是第4號(hào)線程,我開始獲取鎖
我是第5號(hào)線程,我開始獲取鎖
我是第6號(hào)線程,我開始獲取鎖
我是第7號(hào)線程,我開始獲取鎖
我是第8號(hào)線程,我開始獲取鎖
我是第9號(hào)線程,我開始獲取鎖
我是第1號(hào)線程,我已經(jīng)獲取鎖
我是第2號(hào)線程,我已經(jīng)獲取鎖
我是第3號(hào)線程,我已經(jīng)獲取鎖
我是第4號(hào)線程,我已經(jīng)獲取鎖
我是第5號(hào)線程,我已經(jīng)獲取鎖
我是第6號(hào)線程,我已經(jīng)獲取鎖
我是第7號(hào)線程,我已經(jīng)獲取鎖
我是第8號(hào)線程,我已經(jīng)獲取鎖
我是第9號(hào)線程,我已經(jīng)獲取鎖


為什么節(jié)點(diǎn)的名稱要加上uuid,這是框架的英文解釋。

It turns out there is an edge case that exists when creating sequential-ephemeral nodes. The creation can succeed on the server, but the server can crash before the created node name is returned to the client. However, the ZK session is still valid so the ephemeral node is not deleted. Thus, there is no way for the client to determine what node was created for them. 

Even without sequential-ephemeral, however, the create can succeed on the sever but the client (for various reasons) will not know it. 

Putting the create builder into protection mode works around this. The name of the node that is created is prefixed with a GUID. If node creation fails the normal retry mechanism will occur. On the retry, the parent path is first searched for a node that has the GUID in it. If that node is found, it is assumed to be the lost node that was successfully created on the first try and is returned to the caller.

就是說 當(dāng)客戶端創(chuàng)建了一個(gè)節(jié)點(diǎn),這個(gè)創(chuàng)建的過程在zook的服務(wù)器端已經(jīng)成功了,但是在將節(jié)點(diǎn)的路徑返回給客戶端之前服務(wù)器端掛了, 因?yàn)榭蛻舳说膕ession還是有效的,所以這個(gè)節(jié)點(diǎn)不會(huì)刪除, 這樣客戶端就不知道哪個(gè)節(jié)點(diǎn)是它創(chuàng)建的。

當(dāng)客戶端發(fā)生創(chuàng)建失敗的時(shí)候,會(huì)進(jìn)行重試,如果這個(gè)時(shí)候zook已經(jīng)恢復(fù)可用,那么客戶端會(huì)查詢服務(wù)器端所有子節(jié)點(diǎn),然后通過和自己創(chuàng)建的uuid對(duì)比,如果找到了,說明這個(gè)節(jié)點(diǎn)是它之前創(chuàng)建的,那么久直接使用它,不然這個(gè)節(jié)點(diǎn)就會(huì)成為一個(gè)死節(jié)點(diǎn),導(dǎo)致死鎖。

實(shí)現(xiàn)非公平鎖:

重寫創(chuàng)建節(jié)點(diǎn)的方法,

package com.framework.code.demo.zook.lock;

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.locks.StandardLockInternalsDriver;
import org.apache.zookeeper.CreateMode;

public class NoFairLockDriver extends StandardLockInternalsDriver {

	/**
	 * 隨機(jī)數(shù)的長度
	 */
	private int numLength; 
	private static int DEFAULT_LENGTH = 5;
	
	public NoFairLockDriver() {
		this(DEFAULT_LENGTH);
	}
	
	public NoFairLockDriver(int numLength) {
		this.numLength = numLength;
	}
	
    @Override
    public String createsTheLock(CuratorFramework client, String path, byte[] lockNodeBytes) throws Exception
    {
    	String newPath = path + getRandomSuffix();
        String ourPath;
        if ( lockNodeBytes != null )
        {
        	//原來使用的是CreateMode.EPHEMERAL_SEQUENTIAL類型的節(jié)點(diǎn)
        	//節(jié)點(diǎn)名稱最終是這樣的_c_c8e86826-d3dd-46cc-8432-d91aed763c2e-lock-0000000025
        	//其中0000000025是zook服務(wù)器端資自動(dòng)生成的自增序列 從0000000000開始
        	//所以每個(gè)客戶端創(chuàng)建節(jié)點(diǎn)的順序都是按照0,1,2,3這樣遞增的順序排列的,所以他們獲取鎖的順序與他們進(jìn)入的順序是一致的,這也就是所謂的公平鎖
        	//現(xiàn)在我們將有序的編號(hào)換成隨機(jī)的數(shù)字,這樣在獲取鎖的時(shí)候變成非公平鎖了
            ourPath = client.create().creatingParentContainersIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL).forPath(newPath, lockNodeBytes);
            //ourPath = client.create().creatingParentContainersIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(path, lockNodeBytes);
        }
        else
        {
            ourPath = client.create().creatingParentContainersIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL).forPath(newPath);
            //ourPath = client.create().creatingParentContainersIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(path);
        }
        return ourPath;
    }
    
    /**
     * 獲得隨機(jī)數(shù)字符串
     */
    public String getRandomSuffix() {
    	StringBuilder sb = new StringBuilder();
		for (int i = 0; i < numLength; i++) {
			sb.append((int) (Math.random() * 10));
		}
		return sb.toString();
    }
    
}

把我們寫的類注冊(cè)進(jìn)去:

InterProcessMutex lock = new InterProcessMutex(client,"/mylock", new NoFairLockDriver());

還是上面的例子,在跑一邊看結(jié)果,可以看到,獲取鎖的順序已經(jīng)是無序的了,從而實(shí)現(xiàn)了非公平鎖。

我是第1號(hào)線程,我開始獲取鎖
我是第0號(hào)線程,我開始獲取鎖
我是第0號(hào)線程,我已經(jīng)獲取鎖
我是第2號(hào)線程,我開始獲取鎖
我是第3號(hào)線程,我開始獲取鎖
我是第4號(hào)線程,我開始獲取鎖
我是第5號(hào)線程,我開始獲取鎖
我是第6號(hào)線程,我開始獲取鎖
我是第7號(hào)線程,我開始獲取鎖
我是第8號(hào)線程,我開始獲取鎖
我是第9號(hào)線程,我開始獲取鎖
我是第9號(hào)線程,我已經(jīng)獲取鎖
我是第8號(hào)線程,我已經(jīng)獲取鎖
我是第4號(hào)線程,我已經(jīng)獲取鎖
我是第7號(hào)線程,我已經(jīng)獲取鎖
我是第3號(hào)線程,我已經(jīng)獲取鎖
我是第1號(hào)線程,我已經(jīng)獲取鎖
我是第2號(hào)線程,我已經(jīng)獲取鎖
我是第5號(hào)線程,我已經(jīng)獲取鎖
我是第6號(hào)線程,我已經(jīng)獲取鎖

到此,關(guān)于“如何使用ZooKeeper實(shí)現(xiàn)Java跨JVM的分布式鎖 ”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請(qǐng)繼續(xù)關(guān)注億速云網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)砀鄬?shí)用的文章!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI