本文主要分为两部分,第一部分是Zookeeper的安装;第二部分才是SpringBoot和Zookeeper的集成。
第一部分Zookeeper的安装;
系统环境win10,Zookeeper安装版本3.4.14;
首先,下载Zookeeper的压缩包,从https://mirrors.cnnic.cn/apache/zookeeper/zookeeper-3.4.14/下载压缩包,这里使用3.4.14版本,下载后解压;3.3.14版本已经找不到了,在这里只能下载3.4.14版本的压缩包;
第二步,创建数据目录和日志目录;来到解压的文件夹下,建立一个data文件夹和一个log文件夹,data文件夹和log文件夹下分别建立zoo-1、zoo-2、zoo-3三个文件夹;
第三步,创建myid文件;在data的zoo-1、zoo-2、zoo-3文件夹下,分别创建一个myid文件,没有后缀;zoo-1下的myid文件内容为1,zoo-2下的myid文件内容为2,zoo-3下的myid文件内容为3;
第四步,创建配置文件;在conf文件夹下,将zoo_sample.cfg复制三份,分别命名为zoo-1.cfg、zoo-2.cfg、zoo-3.cfg;并为配置文件创建配置内容,这里只贴出第一个配置文件的内容,第二个和第三个与第一个不同的地方就是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
第五步,创建cmd快捷启动服务的窗口;打开bin文件夹,将zkServer.cmd复制成三份,名字分别为zkServer-1.cmd、zkServer-2.cmd、zkServer-3.cmd,每个配置里分别加上对应的配置;
set ZOOCFG=C:\zookeeper-3.4.14\conf\zoo-1.cfg
最后,启动伪集群服务; 以管理员的身份打开cmd窗口,先进入到bin目录下,然后再输入命令启动服务,由于是三个服务,所以需要启动三个cmd窗口进行启动;
zkServer-1.cmd
当有看到下面的消息时,代表服务已经启动成功了。
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
......
注意事项:
- 配置文件里的文件路径一定要用/,而不要用\,结束符也要用/,否则启动会失败;
- cmd文件里的ZOOCFG=,不要写成ZOOCFG%=,否则也会启动失败;
- 这三个服务是是伪集群服务,当启动一个时,不免有报错信息,三个都启动就恢复正常。
- Zookeeper节点数必须是奇数,所以这里创建了最少的节点数,选出其中的一个为leader也就是主节点。
第二部分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>
第二步,创建服务注册的配置文件;
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的服务注册
* @author 程就人生
* @date 2019年10月30日
*/
public class ServiceRegistry {
private static Logger log = LoggerFactory.getLogger(ServiceRegistry.class);
private CountDownLatch latch = new CountDownLatch(1);
//这个可以放到配置文件里,对应Zookeeper已经启动的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 服务器
* @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;
}
/**
* 创建节点
* @param zk
* @param data
*/
private void createNode(ZooKeeper zk, String data) {
try {
//父节点不存在时进行创建
Stat stat = zk.exists(nodePath, true);
if(stat == null){
zk.create(nodePath, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
//这里的第一个参数和3.4.13版本的zookeeper不一样,如果不加父目录,直接就是使用/app/会报错,所以智能加父目录
//CreateMode.EPHEMERAL_SEQUENTIAL,创建临时顺序节点,客户端会话结束后,节点将会被删除
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();
}
}
}
第三步,创建服务发现的配置文件;
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的服务发现
* @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<>();
//这个可以放到配置文件里,对应Zookeeper已经启动的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);
}
}
/**
* 通过服务发现,获取服务提供方的地址
* @return
*/
public String discover() {
String data = null;
int size = serviceAddressList.size();
if (size > 0) {
if (size == 1) {
//只有一个服务提供方
data = serviceAddressList.get(0);
log.info("unique service address :{}", data);
} else {
//使用随机分配法,简单的负载均衡法
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;
}
/**
* 获取服务地址列表
* @param zk
*/
private void watchNode(final ZooKeeper zk) {
try {
//获取子节点列表
List<String> nodeList = zk.getChildren(nodePath,new Watcher() {
@Override
public void process(WatchedEvent event) {
if (event.getType() == Event.EventType.NodeChildrenChanged) {
// 发生子节点变化时再次调用此方法更新服务地址
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){
//服务发现
ServiceDiscovery serviceDiscovery = new ServiceDiscovery();
serviceDiscovery.discover();
}
}
第三步,修改启动类,在启动类启动时进行注册;
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;
/**
* 实现CommandLineRunner接口,把要执行的代码放入到run里,即可在启动后执行
* @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);
//服务注册
ServiceRegistry serviceRegistry = new ServiceRegistry();
String ip = InetAddress.getLocalHost().getHostAddress();
serviceRegistry.register(ip+":"+port);
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
}
最后,测试;启动服务启动类,注册已经启动的服务;最后在服务发现类中运行main,就可以看到返回的ip及端口号地址;
在这里多启动了几个端口,从服务发现里可以看到,启动的端口都被注册到了zookeeper服务器上,这样就可以根据返回的ip+port进行调用具体的服务。