本文主要分為兩部分,第一部分是Zookeeper的安裝;第二部分才是SpringBoot和Zookeeper的集成。
第一部分Zookeeper的安裝;
系統(tǒng)環(huán)境win10,Zookeeper安裝版本3.4.14;
首先,下載Zookeeper的壓縮包,從https://mirrors.cnnic.cn/apache/zookeeper/zookeeper-3.4.14/下載壓縮包,這里使用3.4.14版本,下載后解壓;3.3.14版本已經(jīng)找不到了,在這里只能下載3.4.14版本的壓縮包;
第二步,創(chuàng)建數(shù)據(jù)目錄和日志目錄;來到解壓的文件夾下,建立一個data文件夾和一個log文件夾,data文件夾和log文件夾下分別建立zoo-1、zoo-2、zoo-3三個文件夾;
第三步,創(chuàng)建myid文件;在data的zoo-1、zoo-2、zoo-3文件夾下,分別創(chuàng)建一個myid文件,沒有后綴;zoo-1下的myid文件內(nèi)容為1,zoo-2下的myid文件內(nèi)容為2,zoo-3下的myid文件內(nèi)容為3;
第四步,創(chuàng)建配置文件;在conf文件夾下,將zoo_sample.cfg復(fù)制三份,分別命名為zoo-1.cfg、zoo-2.cfg、zoo-3.cfg;并為配置文件創(chuàng)建配置內(nèi)容,這里只貼出第一個配置文件的內(nèi)容,第二個和第三個與第一個不同的地方就是dataDir、dataLogDir、clientPort的不同,其余的配置都是一樣的。
tickTime=4000
initLimit=10
syncLimit=5
dataDir=C:/zookeeper-3.4.14/data/zoo-1/
dataLogDir=C:/zookeeper-3.4.14/log/zoo-1/
clientPort=2181
server.1=127.0.0.1:2888:3888
server.2=127.0.0.1:2889:3889
server.3=127.0.0.1:2890:3890
第五步,創(chuàng)建cmd快捷啟動服務(wù)的窗口;打開bin文件夾,將zkServer.cmd復(fù)制成三份,名字分別為zkServer-1.cmd、zkServer-2.cmd、zkServer-3.cmd,每個配置里分別加上對應(yīng)的配置;
set ZOOCFG=C:\zookeeper-3.4.14\conf\zoo-1.cfg
最后,啟動偽集群服務(wù); 以管理員的身份打開cmd窗口,先進(jìn)入到bin目錄下,然后再輸入命令啟動服務(wù),由于是三個服務(wù),所以需要啟動三個cmd窗口進(jìn)行啟動;
zkServer-1.cmd
當(dāng)有看到下面的消息時,代表服務(wù)已經(jīng)啟動成功了。
C:\zookeeper-3.4.14\bin>zkServer-3.cmd
C:\zookeeper-3.4.14\bin>call "C:\Program Files\Java\jdk1.8.0_181"\bin\java "-Dzookeeper.log.dir=C:\zookeeper-3.4.14\bin\.." "-Dzookeeper.root.logger=INFO,CONSOLE" -cp "C:\zookeeper-3.4.14\bin\..\build\classes;C:\zookeeper-3.4.14\bin\..\build\lib\*;C:\zookeeper-3.4.14\bin\..\*;C:\zookeeper-3.4.14\bin\..\lib\*;C:\zookeeper-3.4.14\bin\..\conf" org.apache.zookeeper.server.quorum.QuorumPeerMain "C:\zookeeper-3.4.14\conf\zoo-3.cfg"
2019-10-30 20:01:59,025 [myid:] - INFO [main:QuorumPeerConfig@136] - Reading configuration from: C:\zookeeper-3.4.14\conf\zoo-3.cfg
2019-10-30 20:01:59,052 [myid:] - INFO [main:QuorumPeer$QuorumServer@185] - Resolved hostname: 127.0.0.1 to address: /127.0.0.1
2019-10-30 20:01:59,054 [myid:] - INFO [main:QuorumPeer$QuorumServer@185] - Resolved hostname: 127.0.0.1 to address: /127.0.0.1
2019-10-30 20:01:59,058 [myid:] - INFO [main:QuorumPeer$QuorumServer@185] - Resolved hostname: 127.0.0.1 to address: /127.0.0.1
2019-10-30 20:01:59,063 [myid:] - INFO [main:QuorumPeerConfig@398] - Defaulting to majority quorums
2019-10-30 20:01:59,070 [myid:3] - INFO [main:DatadirCleanupManager@78] - autopurge.snapRetainCount set to 3
2019-10-30 20:01:59,071 [myid:3] - INFO [main:DatadirCleanupManager@79] - autopurge.purgeInterval set to 0
2019-10-30 20:01:59,073 [myid:3] - INFO [main:DatadirCleanupManager@101] - Purge task is not scheduled.
2019-10-30 20:01:59,270 [myid:3] - INFO [main:QuorumPeerMain@130] - Starting quorum peer
2019-10-30 20:02:00,172 [myid:3] - INFO [main:ServerCnxnFactory@117] - Using org.apache.zookeeper.server.NIOServerCnxnFactory as server connection factory
2019-10-30 20:02:00,174 [myid:3] - INFO [main:NIOServerCnxnFactory@89] - binding to port 0.0.0.0/0.0.0.0:2183
......
注意事項:
- 配置文件里的文件路徑一定要用/,而不要用\,結(jié)束符也要用/,否則啟動會失??;
- cmd文件里的ZOOCFG=,不要寫成ZOOCFG%=,否則也會啟動失?。?/li>
- 這三個服務(wù)是是偽集群服務(wù),當(dāng)啟動一個時,不免有報錯信息,三個都啟動就恢復(fù)正常。
- Zookeeper節(jié)點數(shù)必須是奇數(shù),所以這里創(chuàng)建了最少的節(jié)點數(shù),選出其中的一個為leader也就是主節(jié)點。
第二部分SpringBoot和Zookeeper的集成
首先,在pom中引入一個必須的架包;
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<!-- Apache下的Zookeeper架包-->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.5.5</version>
</dependency>
第二步,創(chuàng)建服務(wù)注冊的配置文件;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 基于Zookeeper的服務(wù)注冊
* @author 程就人生
* @date 2019年10月30日
*/
public class ServiceRegistry {
private static Logger log = LoggerFactory.getLogger(ServiceRegistry.class);
private CountDownLatch latch = new CountDownLatch(1);
//這個可以放到配置文件里,對應(yīng)Zookeeper已經(jīng)啟動的ip+port
private String registryAddress = "127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183";
private String nodePath = "/app";
private String cnodePath = "/chatting";
private int timeout = 3000;
public ServiceRegistry() {
}
public void register(String data) {
if (data != null) {
ZooKeeper zk = connectServer();
if (zk != null) {
createNode(zk, data);
}
}
}
/**
* 連接 zookeeper 服務(wù)器
* @return
*/
private ZooKeeper connectServer() {
ZooKeeper zk = null;
try {
zk = new ZooKeeper(registryAddress, timeout, new Watcher() {
@Override
public void process(WatchedEvent event) {
if (event.getState() == Event.KeeperState.SyncConnected) {
latch.countDown();
log.info("Watcher.........");
}
}
});
latch.await();
} catch (IOException | InterruptedException e) {
log.error("", e);
e.printStackTrace();
}
return zk;
}
/**
* 創(chuàng)建節(jié)點
* @param zk
* @param data
*/
private void createNode(ZooKeeper zk, String data) {
try {
//父節(jié)點不存在時進(jìn)行創(chuàng)建
Stat stat = zk.exists(nodePath, true);
if(stat == null){
zk.create(nodePath, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
//這里的第一個參數(shù)和3.4.13版本的zookeeper不一樣,如果不加父目錄,直接就是使用/app/會報錯,所以智能加父目錄
//CreateMode.EPHEMERAL_SEQUENTIAL,創(chuàng)建臨時順序節(jié)點,客戶端會話結(jié)束后,節(jié)點將會被刪除
String createPath = zk.create(nodePath+cnodePath, data.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
log.info("create zookeeper node ({} => {} => {})", data, createPath);
} catch (KeeperException | InterruptedException e) {
log.info("", e);
e.printStackTrace();
}
}
}
第三步,創(chuàng)建服務(wù)發(fā)現(xiàn)的配置文件;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadLocalRandom;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 基于Zookeeper的服務(wù)發(fā)現(xiàn)
* @author 程就人生
* @date 2019年10月30日
*/
public class ServiceDiscovery {
private static Logger log = LoggerFactory.getLogger(ServiceDiscovery.class);
private CountDownLatch latch = new CountDownLatch(1);
private volatile List<String> serviceAddressList = new ArrayList<>();
//這個可以放到配置文件里,對應(yīng)Zookeeper已經(jīng)啟動的ip+port
private String registryAddress = "127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183";
private String nodePath = "/app";
private int timeout = 3000;
//注冊中心的地址
public ServiceDiscovery() {
ZooKeeper zk = connectServer();
if (zk != null) {
watchNode(zk);
}
}
/**
* 通過服務(wù)發(fā)現(xiàn),獲取服務(wù)提供方的地址
* @return
*/
public String discover() {
String data = null;
int size = serviceAddressList.size();
if (size > 0) {
if (size == 1) {
//只有一個服務(wù)提供方
data = serviceAddressList.get(0);
log.info("unique service address :{}", data);
} else {
//使用隨機分配法,簡單的負(fù)載均衡法
data = serviceAddressList.get(ThreadLocalRandom.current().nextInt(size));
log.info("choose an address : {}",data);
}
}
return data;
}
/**
* 連接 zookeeper
* @return
*/
private ZooKeeper connectServer() {
ZooKeeper zk = null;
try {
zk = new ZooKeeper(registryAddress, timeout, new Watcher() {
@Override
public void process(WatchedEvent event) {
if (event.getState() == Watcher.Event.KeeperState.SyncConnected) {
latch.countDown();
}
}
});
latch.await();
} catch (IOException | InterruptedException e) {
log.error("", e);
e.printStackTrace();
}
return zk;
}
/**
* 獲取服務(wù)地址列表
* @param zk
*/
private void watchNode(final ZooKeeper zk) {
try {
//獲取子節(jié)點列表
List<String> nodeList = zk.getChildren(nodePath,new Watcher() {
@Override
public void process(WatchedEvent event) {
if (event.getType() == Event.EventType.NodeChildrenChanged) {
// 發(fā)生子節(jié)點變化時再次調(diào)用此方法更新服務(wù)地址
watchNode(zk);
}
}
});
List<String> dataList = new ArrayList<>();
for (String node :nodeList) {
byte[] bytes = zk.getData(nodePath + "/" + node, false, null);
dataList.add(new String(bytes));
}
log.info("node data: {}", dataList);
this.serviceAddressList = dataList;
}catch(KeeperException|InterruptedException e){
log.error("", e);
e.printStackTrace();
}
}
public static void main(String[] agro){
//服務(wù)發(fā)現(xiàn)
ServiceDiscovery serviceDiscovery = new ServiceDiscovery();
serviceDiscovery.discover();
}
}
第三步,修改啟動類,在啟動類啟動時進(jìn)行注冊;
import java.net.InetAddress;
import java.net.UnknownHostException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import com.example.demo.netty.NettyServer;
import com.example.demo.zookeeper.ServiceRegistry;
/**
* 實現(xiàn)CommandLineRunner接口,把要執(zhí)行的代碼放入到run里,即可在啟動后執(zhí)行
* @author 程就人生
* @date 2019年10月30日
*/
@SpringBootApplication
public class SpringbootZookeeperApplication implements CommandLineRunner{
@Value("${im.server.port}")
private int port;
public static void main(String[] args) {
SpringApplication.run(SpringbootZookeeperApplication.class, args);
}
@Override
public void run(String... args) throws Exception {
try {
NettyServer server = new NettyServer();
server.start(port);
//服務(wù)注冊
ServiceRegistry serviceRegistry = new ServiceRegistry();
String ip = InetAddress.getLocalHost().getHostAddress();
serviceRegistry.register(ip+":"+port);
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
}
最后,測試;啟動服務(wù)啟動類,注冊已經(jīng)啟動的服務(wù);最后在服務(wù)發(fā)現(xiàn)類中運行main,就可以看到返回的ip及端口號地址;


在這里多啟動了幾個端口,從服務(wù)發(fā)現(xiàn)里可以看到,啟動的端口都被注冊到了zookeeper服務(wù)器上,這樣就可以根據(jù)返回的ip+port進(jìn)行調(diào)用具體的服務(wù)。