本文內容包括:zookeeper的事件監聽機制、集羣搭建、一致性協議、leader選舉、角色等
傳送門(已完結):
認認真真學習zookeeper(一)
認認真真學習zookeeper(二)
認認真真學習zookeeper(三)
文章目錄
1. zookeeper 事件監聽機制
1.1 watcher概念
zookeeper提供了數據的發佈/訂閱功能,多個訂閱者可同時監聽某一特定主題對象,當該主題對象的自身狀態發生變化時(例如節點內容改變、節點下的子節點列表改變等),會實時、主動通知所有訂閱者
zookeeper採用了Watcher機制實現數據的發佈/訂閱功能。該機制在被訂閱對象發生變化時會異步通知客戶端,因此客戶端不必在Watcher註冊後輪詢阻塞,從而減輕了客戶端壓力。
watcher機制實際上與觀察者模式類似,也可看作是一種觀察者模式在分佈式場景下的實現方式。
1.2 watcher架構
Watcher實現由三個部分組成:
- Zookeeper服務端
- Zookeeper客戶端
- 客戶端的ZKWatchManager對象
客戶端首先將Watcher註冊到服務端,同時將Watcher對象保存到客戶端的Watch管理器中。當ZooKeeper服務端監聽的數據狀態發生變化時,服務端會主動通知客戶端,接着客戶端的Watch管理器會觸發相關Watcher來回調相應處理邏輯,從而完成整體的數據發佈/訂閱流程。
1.3 watcher特性
特性 | 說明 |
---|---|
一次性 | watcher是一次性的,一旦被觸發就會移除,再次使用時需要重新註冊 |
客戶端順序回調 | watcher回調是順序串行化執行的,只有回調後客戶端才能看到最新的數據狀態。一個watcher回調邏輯不應該太多,以免影響別的watcher執行 |
輕量級 | WatchEvent是最小的通信單元,結構上只包含通知狀態、事件類型和節點路徑,並不會告訴數據節點變化前後的具體內容; |
時效性 | watcher只有在當前session徹底失效時纔會無效,若在session有效期內快速重連成功,則watcher依然存在,仍可接收到通知; |
1.4 watcher接口設計
Watcher是一個接口,任何實現了Watcher接口的類就是一個新的Watcher。Watcher內部包含了兩個枚舉類:KeeperState、EventType
- Watcher通知狀態(KeeperState)
KeeperState是客戶端與服務端連接狀態發生變化時對應的通知類型。路徑爲org.apache.zookeeper.Watcher.Event.KeeperState,是一個枚舉類,其枚舉屬性如下:
枚舉屬性 | 說明 |
---|---|
SyncConnected | 客戶端與服務器正常連接時 |
Disconnected | 客戶端與服務器斷開連接時 |
Expired | 會話session失效時 |
AuthFailed | 身份認證失敗時 |
- Watcher事件類型(EventType)
EventType是數據節點(znode)發生變化時對應的通知類型。EventType變化時KeeperState永遠處於SyncConnected通知狀態下;當KeeperState發生變化時,EventType永遠爲None。其路徑org.apache.zookeeper.Watcher.Event.EventType,是一個枚舉類,枚舉屬性如下:
枚舉屬性 | 說明 |
---|---|
None | 無 |
NodeCreated | Watcher監聽的數據節點被創建時 |
NodeDeleted | Watcher監聽的數據節點被刪除時 |
NodeDataChanged | Watcher監聽的數據節點內容發生變更時(無論內容數據是否變化) |
NodeChildrenChanged | Watcher監聽的數據節點的子節點列表發生變更時 |
注:客戶端接收到的相關事件通知中只包含狀態及類型等信息,不包括節點變化前後的具體內容,變化前的數據需業務自身存儲,變化後的數據需調用get等方法重新獲取;
1.5 捕獲相應的事件
上面講到zookeeper客戶端連接的狀態和zookeeper對znode節點監聽的事件類型,下面我們來講解如何建立zookeeper的watcher監聽。在zookeeper中採用
zk.getChildren(path, watch)、zk.exists(path, watch)、zk.getData(path, watcher, stat)
這樣的方式爲某個znode註冊監聽。
下表以node-x節點爲例,說明調用的註冊方法和可監聽事件間的關係:
註冊方式 | Created | ChildrenChanged | Changed | Deleted |
---|---|---|---|---|
zk.exists(“/node-x”,watcher) | 可監控 | 可監控 | 可監控 | |
zk.getData(“/node-x”,watcher) | 可監控 | 可監控 | ||
zk.getChildren(“/node-x”,watcher) | 可監控 | 可監控 |
1.6 註冊watcher的方法
1.6.1 客服端與服務器的連接狀態
KeeperState 通知狀態
SyncConnected:客戶端與服務器正常連接時
Disconnected:客戶端與服務器斷開連接時
Expired:會話session失效時
AuthFailed:身份認證失敗時
事件類型爲:None
案例:
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import java.util.concurrent.CountDownLatch;
public class ZKConnectionWatcher implements Watcher {
// 計數器對象
private static CountDownLatch countDownLatch = new CountDownLatch(1);
// 連接對象
private static ZooKeeper zooKeeper;
@Override
public void process(WatchedEvent event) {
try {
// 事件類型
if (event.getType() == Event.EventType.None) {
if (event.getState() == Event.KeeperState.SyncConnected) {
System.out.println("連接創建成功!");
countDownLatch.countDown();
} else if (event.getState() == Event.KeeperState.Disconnected) {
System.out.println("斷開連接!");
} else if (event.getState() == Event.KeeperState.Expired) {
System.out.println("會話超時!");
zooKeeper = new ZooKeeper("192.168.188.133:2181", 5000, new ZKConnectionWatcher());
} else if (event.getState() == Event.KeeperState.AuthFailed) {
System.out.println("認證失敗!");
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
public static void main(String[] args) {
try {
zooKeeper = new ZooKeeper("192.168.188.133:2181", 5000, new ZKConnectionWatcher());
// 阻塞線程等待連接的創建
countDownLatch.await();
// 會話id
System.out.println(zooKeeper.getSessionId());
// 添加授權用戶
zooKeeper.addAuthInfo("digest1","sky1:123456".getBytes());
byte [] bs=zooKeeper.getData("/node12",false,null);
System.out.println(new String(bs));
Thread.sleep(50000);
zooKeeper.close();
System.out.println("結束");
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
1.6.2 檢查節點是否存在
// 使用連接對象的監視器
exists(String path, boolean b)
// 自定義監視器
exists(String path, Watcher w)
// NodeCreated:節點創建
// NodeDeleted:節點刪除
// NodeDataChanged:節點內容發生變化
- path- znode路徑。
- b- 是否使用連接對象中註冊的監視器。
- w-監視器對象。
案例:
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
public class ZKWatcherExists {
private String IP = "192.168.188.133:2181";
private ZooKeeper zooKeeper = null;
@Before
public void before() throws IOException, InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(1);
// 連接zookeeper客戶端
zooKeeper = new ZooKeeper(IP, 6000, new Watcher() {
@Override
public void process(WatchedEvent event) {
System.out.println("連接對象的參數!");
// 連接成功
if (event.getState() == Event.KeeperState.SyncConnected) {
countDownLatch.countDown();
}
System.out.println("path=" + event.getPath());
System.out.println("eventType=" + event.getType());
}
});
countDownLatch.await();
}
@After
public void after() throws InterruptedException {
zooKeeper.close();
}
@Test
public void watcherExists1() throws KeeperException, InterruptedException {
// arg1:節點的路徑
// arg2:使用連接對象中的watcher
zooKeeper.exists("/watcher1", true);
Thread.sleep(50000);
System.out.println("結束");
}
@Test
public void watcherExists2() throws KeeperException, InterruptedException {
// arg1:節點的路徑
// arg2:自定義watcher對象
zooKeeper.exists("/watcher1", new Watcher() {
@Override
public void process(WatchedEvent event) {
System.out.println("自定義watcher");
System.out.println("path=" + event.getPath());
System.out.println("eventType=" + event.getType());
}
});
Thread.sleep(50000);
System.out.println("結束");
}
@Test
public void watcherExists3() throws KeeperException, InterruptedException {
// watcher一次性
Watcher watcher = new Watcher() {
@Override
public void process(WatchedEvent event) {
try {
System.out.println("自定義watcher");
System.out.println("path=" + event.getPath());
System.out.println("eventType=" + event.getType());
zooKeeper.exists("/watcher1", this);
} catch (Exception ex) {
ex.printStackTrace();
}
}
};
zooKeeper.exists("/watcher1", watcher);
Thread.sleep(80000);
System.out.println("結束");
}
@Test
public void watcherExists4() throws KeeperException, InterruptedException {
// 註冊多個監聽器對象
zooKeeper.exists("/watcher1", new Watcher() {
@Override
public void process(WatchedEvent event) {
System.out.println("1");
System.out.println("path=" + event.getPath());
System.out.println("eventType=" + event.getType());
}
});
zooKeeper.exists("/watcher1", new Watcher() {
@Override
public void process(WatchedEvent event) {
System.out.println("2");
System.out.println("path=" + event.getPath());
System.out.println("eventType=" + event.getType());
}
});
Thread.sleep(80000);
System.out.println("結束");
}
}
1.6.3 查看節點
// 使用連接對象的監視器
getData(String path, boolean b, Stat stat)
// 自定義監視器
getData(String path, Watcher w, Stat stat)
// NodeDeleted:節點刪除
// NodeDataChanged:節點內容發生變化
- path- znode路徑。
- b- 是否使用連接對象中註冊的監視器。
- w-監視器對象。
- stat- 返回znode的元數據。
案例:
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
public class ZKWatcherGetData {
String IP = "192.168.188.133:2181";
ZooKeeper zooKeeper = null;
@Before
public void before() throws IOException, InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(1);
// 連接zookeeper客戶端
zooKeeper = new ZooKeeper(IP, 6000, new Watcher() {
@Override
public void process(WatchedEvent event) {
System.out.println("連接對象的參數!");
// 連接成功
if (event.getState() == Event.KeeperState.SyncConnected) {
countDownLatch.countDown();
}
System.out.println("path=" + event.getPath());
System.out.println("eventType=" + event.getType());
}
});
countDownLatch.await();
}
@After
public void after() throws InterruptedException {
zooKeeper.close();
}
@Test
public void watcherGetData1() throws KeeperException, InterruptedException {
// arg1:節點的路徑
// arg2:使用連接對象中的watcher
zooKeeper.getData("/watcher2", true, null);
Thread.sleep(50000);
System.out.println("結束");
}
@Test
public void watcherGetData2() throws KeeperException, InterruptedException {
// arg1:節點的路徑
// arg2:自定義watcher對象
zooKeeper.getData("/watcher2", new Watcher() {
@Override
public void process(WatchedEvent event) {
System.out.println("自定義watcher");
System.out.println("path=" + event.getPath());
System.out.println("eventType=" + event.getType());
}
}, null);
Thread.sleep(50000);
System.out.println("結束");
}
@Test
public void watcherGetData3() throws KeeperException, InterruptedException {
// 一次性
Watcher watcher = new Watcher() {
@Override
public void process(WatchedEvent event) {
try {
System.out.println("自定義watcher");
System.out.println("path=" + event.getPath());
System.out.println("eventType=" + event.getType());
if(event.getType()==Event.EventType.NodeDataChanged) {
zooKeeper.getData("/watcher2", this, null);
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
};
zooKeeper.getData("/watcher2", watcher, null);
Thread.sleep(50000);
System.out.println("結束");
}
@Test
public void watcherGetData4() throws KeeperException, InterruptedException {
// 註冊多個監聽器對象
zooKeeper.getData("/watcher2", new Watcher() {
@Override
public void process(WatchedEvent event) {
try {
System.out.println("1");
System.out.println("path=" + event.getPath());
System.out.println("eventType=" + event.getType());
if(event.getType()==Event.EventType.NodeDataChanged) {
zooKeeper.getData("/watcher2", this, null);
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
},null);
zooKeeper.getData("/watcher2", new Watcher() {
@Override
public void process(WatchedEvent event) {
try {
System.out.println("2");
System.out.println("path=" + event.getPath());
System.out.println("eventType=" + event.getType());
if(event.getType()==Event.EventType.NodeDataChanged) {
zooKeeper.getData("/watcher2", this, null);
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
},null);
Thread.sleep(50000);
System.out.println("結束");
}
}
1.6.4 查看子節點
// 使用連接對象的監視器
getChildren(String path, boolean b)
// 自定義監視器
getChildren(String path, Watcher w)
// NodeChildrenChanged:子節點發生變化
// NodeDeleted:節點刪除
- path- znode路徑。
- b- 是否使用連接對象中註冊的監視器。
- w-監視器對象。
案例:
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.CountDownLatch;
public class ZKWatcherGetChild {
String IP = "192.168.188.133:2181";
ZooKeeper zooKeeper = null;
@Before
public void before() throws IOException, InterruptedException {
CountDownLatch connectedSemaphore = new CountDownLatch(1);
// 連接zookeeper客戶端
zooKeeper = new ZooKeeper(IP, 5000, new Watcher() {
@Override
public void process(WatchedEvent event) {
System.out.println("連接對象的參數!");
// 連接成功
if (event.getState() == Event.KeeperState.SyncConnected) {
connectedSemaphore.countDown();
}
System.out.println("path=" + event.getPath());
System.out.println("eventType=" + event.getType());
}
});
connectedSemaphore.await();
}
@After
public void after() throws InterruptedException {
zooKeeper.close();
}
@Test
public void watcherGetChild1() throws KeeperException, InterruptedException {
// arg1:節點的路徑
// arg2:使用連接對象中的watcher
zooKeeper.getChildren("/watcher3", true);
Thread.sleep(50000);
System.out.println("結束");
}
@Test
public void watcherGetChild2() throws KeeperException, InterruptedException {
// arg1:節點的路徑
// arg2:自定義watcher
zooKeeper.getChildren("/watcher3", new Watcher() {
@Override
public void process(WatchedEvent event) {
System.out.println("自定義watcher");
System.out.println("path=" + event.getPath());
System.out.println("eventType=" + event.getType());
}
});
Thread.sleep(50000);
System.out.println("結束");
}
@Test
public void watcherGetChild3() throws KeeperException, InterruptedException {
// 一次性
Watcher watcher = new Watcher() {
@Override
public void process(WatchedEvent event) {
try {
System.out.println("自定義watcher");
System.out.println("path=" + event.getPath());
System.out.println("eventType=" + event.getType());
if (event.getType() == Event.EventType.NodeChildrenChanged) {
zooKeeper.getChildren("/watcher3", this);
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
};
zooKeeper.getChildren("/watcher3", watcher);
Thread.sleep(50000);
System.out.println("結束");
}
@Test
public void watcherGetChild4() throws KeeperException, InterruptedException {
// 多個監視器對象
zooKeeper.getChildren("/watcher3", new Watcher() {
@Override
public void process(WatchedEvent event) {
try {
System.out.println("1");
System.out.println("path=" + event.getPath());
System.out.println("eventType=" + event.getType());
if (event.getType() == Event.EventType.NodeChildrenChanged) {
zooKeeper.getChildren("/watcher3", this);
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
});
zooKeeper.getChildren("/watcher3", new Watcher() {
@Override
public void process(WatchedEvent event) {
try {
System.out.println("2");
System.out.println("path=" + event.getPath());
System.out.println("eventType=" + event.getType());
if (event.getType() == Event.EventType.NodeChildrenChanged) {
zooKeeper.getChildren("/watcher3", this);
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
});
Thread.sleep(50000);
System.out.println("結束");
}
}
1.7 配置中心案例
工作中有這樣的一個場景: 數據庫用戶名和密碼信息放在一個配置文件中,應用讀取該配置文件,配置文件信息放入緩存。
若數據庫的用戶名和密碼改變時候,還需要重新加載緩存,比較麻煩,通過ZooKeeper可以輕鬆完成,當數據庫發生變化時自動完成緩存同步。
設計思路:
- 連接zookeeper服務器
- 讀取zookeeper中的配置信息,註冊watcher監聽器,存入本地變量
- 當zookeeper中的配置信息發生變化時,通過watcher的回調方法捕獲數據變化事件
- 重新獲取配置信息
案例:
import java.util.concurrent.CountDownLatch;
import com.itcast.watcher.ZKConnectionWatcher;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.Watcher.Event.EventType;
import org.apache.zookeeper.ZooKeeper;
public class MyConfigCenter implements Watcher {
// zk的連接串
private String IP = "192.168.188.133:2181";
// 計數器對象
private CountDownLatch countDownLatch = new CountDownLatch(1);
// 連接對象
private static ZooKeeper zooKeeper;
// 用於本地化存儲配置信息
private String url;
private String username;
private String password;
@Override
public void process(WatchedEvent event) {
try {
// 捕獲事件狀態
if (event.getType() == EventType.None) {
if (event.getState() == Event.KeeperState.SyncConnected) {
System.out.println("連接成功");
countDownLatch.countDown();
} else if (event.getState() == Event.KeeperState.Disconnected) {
System.out.println("連接斷開!");
} else if (event.getState() == Event.KeeperState.Expired) {
System.out.println("連接超時!");
// 超時後服務器端已經將連接釋放,需要重新連接服務器端
zooKeeper = new ZooKeeper(IP, 6000,
new ZKConnectionWatcher());
} else if (event.getState() == Event.KeeperState.AuthFailed) {
System.out.println("驗證失敗!");
}
// 當配置信息發生變化時
} else if (event.getType() == EventType.NodeDataChanged) {
initValue();
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
// 構造方法
private MyConfigCenter() {
try {
// 創建連接對象
zooKeeper = new ZooKeeper(IP, 5000, this);
// 阻塞線程,等待連接的創建成功
countDownLatch.await();
initValue();
}catch (Exception e){
e.printStackTrace();
}
}
// 連接zookeeper服務器,讀取配置信息
private void initValue() {
try {
// 讀取配置信息
this.url = new String(zooKeeper.getData("/config/url", true, null));
this.username = new String(zooKeeper.getData("/config/username", true, null));
this.password = new String(zooKeeper.getData("/config/password", true, null));
} catch (Exception ex) {
ex.printStackTrace();
}
}
public static void main(String[] args) {
try {
MyConfigCenter myConfigCenter = new MyConfigCenter();
for (int i = 1; i <= 20; i++) {
Thread.sleep(5000);
System.out.println("url:"+myConfigCenter.getUrl());
System.out.println("username:"+myConfigCenter.getUsername());
System.out.println("password:"+myConfigCenter.getPassword());
System.out.println("########################################");
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
1.8 生成分佈式唯一ID
在過去的單庫單表型系統中,通常可以使用數據庫字段自帶的auto_increment屬性來自動爲每條記錄生成一個唯一的ID。但是分庫分表後,就無法在依靠數據庫的auto_increment屬性來唯一標識一條記錄了。此時我們就可以用zookeeper在分佈式環境下生成全局唯一ID。
設計思路:
- 連接zookeeper服務器
- 指定路徑生成臨時有序節點
- 取序列號及爲分佈式環境下的唯一ID
案例:
import java.util.concurrent.CountDownLatch;
import com.itcast.watcher.ZKConnectionWatcher;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.Watcher.Event.KeeperState;
import org.apache.zookeeper.ZooDefs.Ids;
import org.apache.zookeeper.ZooKeeper;
public class GloballyUniqueId implements Watcher {
// zk的連接串
private String IP = "192.168.188.133:2181";
// 計數器對象
private CountDownLatch countDownLatch = new CountDownLatch(1);
// 用戶生成序號的節點
private String defaultPath = "/uniqueId";
// 連接對象
private ZooKeeper zooKeeper;
@Override
public void process(WatchedEvent event) {
try {
// 捕獲事件狀態
if (event.getType() == Event.EventType.None) {
if (event.getState() == KeeperState.SyncConnected) {
System.out.println("連接成功");
countDownLatch.countDown();
} else if (event.getState() == KeeperState.Disconnected) {
System.out.println("連接斷開!");
} else if (event.getState() == KeeperState.Expired) {
System.out.println("連接超時!");
// 超時後服務器端已經將連接釋放,需要重新連接服務器端
zooKeeper = new ZooKeeper(IP, 6000,
new ZKConnectionWatcher());
} else if (event.getState() == KeeperState.AuthFailed) {
System.out.println("驗證失敗!");
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
// 構造方法
private GloballyUniqueId() {
try {
//打開連接
zooKeeper = new ZooKeeper(IP, 5000, this);
// 阻塞線程,等待連接的創建成功
countDownLatch.await();
} catch (Exception ex) {
ex.printStackTrace();
}
}
// 生成id的方法
private String getUniqueId() {
String path = "";
try {
//創建臨時有序節點
path = zooKeeper.create(defaultPath, new byte[0], Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
} catch (Exception ex) {
ex.printStackTrace();
}
// /uniqueId0000000001
return path.substring(9);
}
public static void main(String[] args) {
GloballyUniqueId globallyUniqueId = new GloballyUniqueId();
for (int i = 1; i <= 5; i++) {
String id = globallyUniqueId.getUniqueId();
System.out.println(id);
}
}
}
1.9 分佈式鎖
分佈式鎖有多種實現方式,比如通過數據庫、redis都可實現。作爲分佈式協同工具ZooKeeper,當然也有着標準的實現方式。下面介紹在zookeeper中如何實現排他鎖。
設計思路:
- 每個客戶端往/Locks下創建臨時有序節點/Locks/Lock_000000001
- 客戶端取得/Locks下子節點,並進行排序,判斷排在最前面的是否爲自己,如果自己的鎖節點在第一位,代表獲取鎖成功
- 如果自己的鎖節點不在第一位,則監聽自己前一位的鎖節點。例如,自己鎖節點Lock_000000001
- 當前一位鎖節點(Lock_000000002)的邏輯
- 監聽客戶端重新執行第2步邏輯,判斷自己是否獲得了鎖
案例:
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
public class MyLock {
// zk的連接串
private String IP = "192.168.188.133:2181";
// 計數器對象
private CountDownLatch countDownLatch = new CountDownLatch(1);
//ZooKeeper配置信息
private ZooKeeper zooKeeper;
private static final String LOCK_ROOT_PATH = "/Locks";
private static final String LOCK_NODE_NAME = "Lock_";
private String lockPath;
// 打開zookeeper連接
MyLock() {
try {
zooKeeper = new ZooKeeper(IP, 5000, new Watcher() {
@Override
public void process(WatchedEvent event) {
if (event.getType() == Event.EventType.None) {
if (event.getState() == Event.KeeperState.SyncConnected) {
System.out.println("連接成功!");
countDownLatch.countDown();
}
}
}
});
countDownLatch.await();
} catch (Exception ex) {
ex.printStackTrace();
}
}
//獲取鎖
public void acquireLock() throws Exception {
//創建鎖節點
createLock();
//嘗試獲取鎖
attemptLock();
}
//創建鎖節點
private void createLock() throws Exception {
//判斷Locks是否存在,不存在創建
Stat stat = zooKeeper.exists(LOCK_ROOT_PATH, false);
if (stat == null) {
zooKeeper.create(LOCK_ROOT_PATH, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
// 創建臨時有序節點
lockPath = zooKeeper.create(LOCK_ROOT_PATH + "/" + LOCK_NODE_NAME, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println("節點創建成功:" + lockPath);
}
//監視器對象,監視上一個節點是否被刪除
private final Watcher watcher = new Watcher() {
@Override
public void process(WatchedEvent event) {
if (event.getType() == Event.EventType.NodeDeleted) {
synchronized (this) {
notifyAll();
}
}
}
};
//嘗試獲取鎖
private void attemptLock() throws Exception {
// 獲取Locks節點下的所有子節點
List<String> list = zooKeeper.getChildren(LOCK_ROOT_PATH, false);
// 對子節點進行排序
Collections.sort(list);
// /Locks/Lock_000000001
int index = list.indexOf(lockPath.substring(LOCK_ROOT_PATH.length() + 1));
if (index == 0) {
System.out.println("獲取鎖成功!");
} else {
// 上一個節點的路徑
String path = list.get(index - 1);
Stat stat = zooKeeper.exists(LOCK_ROOT_PATH + "/" + path, watcher);
if (stat == null) {
attemptLock();
} else {
synchronized (watcher) {
watcher.wait();
}
attemptLock();
}
}
}
//釋放鎖
public void releaseLock() throws Exception {
//刪除臨時有序節點
zooKeeper.delete(this.lockPath,-1);
zooKeeper.close();
System.out.println("鎖已經釋放:"+this.lockPath);
}
public static void main(String[] args) {
try {
MyLock myLock = new MyLock();
myLock.createLock();
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
public class TicketSeller {
private void sell(){
System.out.println("售票開始");
// 線程隨機休眠數毫秒,模擬現實中的費時操作
int sleepMillis = 5000;
try {
//代表複雜邏輯執行了一段時間
Thread.sleep(sleepMillis);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("售票結束");
}
private void sellTicketWithLock() throws Exception {
MyLock lock = new MyLock();
// 獲取鎖
lock.acquireLock();
sell();
//釋放鎖
lock.releaseLock();
}
public static void main(String[] args) throws Exception {
TicketSeller ticketSeller = new TicketSeller();
for(int i=0;i<10;i++){
ticketSeller.sellTicketWithLock();
}
}
}
2. zookeeper 集羣搭建
單機環境下,jdk、zookeeper 安裝完畢,基於一臺虛擬機,進行zookeeper僞集羣搭建,zookeeper集羣中包含3個節點,節點對外提供服務端口號分別爲2181、2182、2183
- 基於zookeeper-3.4.9複製三份zookeeper安裝好的服務器文件,目錄名稱分別爲zookeeper2181、zookeeper2182、zookeeper2183
cp -r zookeeper-3.4.9 zookeeper2181
cp -r zookeeper-3.4.9 zookeeper2182
cp -r zookeeper-3.4.9 zookeeper2183
- 修改zookeeper2181服務器對應配置文件。
#服務器對應端口號
clientPort=2181
#數據快照文件所在路徑
dataDir=/opt/zookeeper2181/data
#集羣配置信息
#server.A=B:C:D
#A:是一個數字,表示這個是服務器的編號
#B:是這個服務器的ip地址
#C:Zookeeper服務器之間的通信端口
#D:Leader選舉的端口
server.1=192.168.188.133:2287:3387
server.2=192.168.188.133:2288:3388
server.3=192.168.188.133:2289:3389
- 在 上一步 dataDir 指定的目錄下,創建 myid 文件,然後在該文件添加上一步server 配置的對應 A 數字。
#zookeeper2181對應的數字爲1
#/opt/zookeeper2181/data目錄下執行命令
echo "1" > myid
- zookeeper2182、zookeeper2183參照步驟2/3進行相應配置
- 分別啓動三臺服務器,檢驗集羣狀態
啓動命令:
./zkServer.sh start
登錄命令:
./zkCli.sh -server 192.168.188.133:2181
./zkCli.sh -server 192.168.188.133:2182
./zkCli.sh -server 192.168.188.133:2183
3. 一致性協議:zab協議
zab協議 的全稱是 Zookeeper Atomic Broadcast (zookeeper原子廣播)。zookeeper 是通過 zab協議來保證分佈式事務的最終一致性
基於zab協議,zookeeper集羣中的角色主要有以下三類,如下表所示:
zab廣播模式工作原理,通過類似兩階段提交協議的方式解決數據一致性:
- leader從客戶端收到一個寫請求
- leader生成一個新的事務併爲這個事務生成一個唯一的ZXID
- leader將這個事務提議(propose)發送給所有的follows節點
- follower節點將收到的事務請求加入到歷史隊列(history queue)中,併發送ack給leader
- 當leader收到大多數follower(半數以上節點)的ack消息,leader會發送commit請求
- 當follower收到commit請求時,從歷史隊列中將事務請求commit
4. zookeeper的leader選舉
4.1 服務器狀態
looking:尋找leader狀態。當服務器處於該狀態時,它會認爲當前集羣中沒有leader,因此需要進入leader選舉狀態。
leading: 領導者狀態。表明當前服務器角色是leader。
following: 跟隨者狀態。表明當前服務器角色是follower。
observing:觀察者狀態。表明當前服務器角色是observer。
4.2 服務器啓動時期的leader選舉
在集羣初始化階段,當有一臺服務器server1啓動時,其單獨無法進行和完成leader選舉,當第二臺服務器server2啓動時,此時兩臺機器可以相互通信,每臺機器都試圖找到leader,於是進入leader選舉過程。選舉過程如下:
- 每個server發出一個投票。由於是初始情況,server1和server2都會將自己作爲leader服務器來進行投票,每次投票會包含所推舉的服務器的myid和zxid,使用(myid, zxid)來表示,此時server1的投票爲(1, 0),server2的投票爲(2, 0),然後各自將這個投票發給集羣中其他機器。
- 集羣中的每臺服務器接收來自集羣中各個服務器的投票。
- 處理投票。針對每一個投票,服務器都需要將別人的投票和自己的投票進行pk,pk規則如下:
- 優先檢查zxid。zxid比較大的服務器優先作爲leader。
- 如果zxid相同,那麼就比較myid。myid較大的服務器作爲leader服務器。
對於Server1而言,它的投票是(1, 0),接收Server2的投票爲(2, 0),首先會比較兩者的zxid,均爲0,再比較myid,此時server2的myid最大,於是更新自己的投票爲(2, 0),然後重新投票,對於server2而言,其無須更新自己的投票,只是再次向集羣中所有機器發出上一次投票信息即可。
- 統計投票。每次投票後,服務器都會統計投票信息,判斷是否已經有過半機器接受到相同的投票信息,對於server1、server2而言,都統計出集羣中已經有兩臺機器接受了(2, 0)的投票信息,此時便認爲已經選出了leader
- 改變服務器狀態。一旦確定了leader,每個服務器就會更新自己的狀態,如果是follower,那麼就變更爲following,如果是leader,就變更爲leading。
4.3 服務器運行時期的Leader選舉
在zookeeper運行期間,leader與非leader服務器各司其職,即便當有非leader服務器宕機或新加入,此時也不會影響leader,但是一旦leader服務器掛了,那麼整個集羣將暫停對外服務,進入新一輪leader選舉,其過程和啓動時期的Leader選舉過程基本一致。
假設正在運行的有server1、server2、server3三臺服務器,當前leader是server2,若某一時刻leader掛了,此時便開始Leader選舉。選舉過程如下:
- 變更狀態。leader掛後,餘下的服務器都會將自己的服務器狀態變更爲looking,然後開始進入leader選舉過程。
- 每個server會發出一個投票。在運行期間,每個服務器上的zxid可能不同,此時假定server1的zxid爲122,server3的zxid爲122,在第一輪投票中,server1和server3都會投自己,產生投票(1, 122),(3, 122),然後各自將投票發送給集羣中所有機器。
- 接收來自各個服務器的投票。與啓動時過程相同
- 處理投票。與啓動時過程相同,此時,server3將會成爲leader。
- 統計投票。與啓動時過程相同。
- 改變服務器的狀態。與啓動時過程相同。
5. observer角色及其配置
observer角色特點:
- 不參與集羣的leader選舉
- 不參與集羣中寫數據時的ack反饋
爲了使用observer角色,在任何想變成observer角色的配置文件中加入如下配置:
peerType=observer
並在所有server的配置文件中,配置成observer模式的server的那行配置追加:observer,例如:
server.3=192.168.188.133:2289:3389:observer
6. zookeeperAPI連接集羣
ZooKeeper(String connectionString, int sessionTimeout, Watcher watcher)
- connectionString - zooKeeper集合主機。
- sessionTimeout - 會話超時(以毫秒爲單位)。
- watcher - 實現“監視器”界面的對象。ZooKeeper集合通過監視器對象返回連接狀態。
案例:
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import java.util.concurrent.CountDownLatch;
public class ZookeeperConnection {
public static void main(String[] args) {
try {
// 計數器對象
CountDownLatch countDownLatch=new CountDownLatch(1);
// arg1:服務器的ip和端口
// arg2:客戶端與服務器之間的會話超時時間 以毫秒爲單位的
// arg3:監視器對象
ZooKeeper zooKeeper=new ZooKeeper("192.168.188.134:2181,192.168.188.134:2182,192.168.188.134:2183",
5000, new Watcher() {
@Override
public void process(WatchedEvent event) {
if(event.getState()==Event.KeeperState.SyncConnected) {
System.out.println("連接創建成功!");
countDownLatch.countDown();
}
}
});
// 主線程阻塞等待連接對象的創建成功
countDownLatch.await();
// 會話編號
System.out.println(zooKeeper.getSessionId());
zooKeeper.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
}