SpringBoot集成Zookeeper,实现服务的注册与发现踩坑

本文主要分为两部分,第一部分是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
......

注意事项:

  1. 配置文件里的文件路径一定要用/,而不要用\,结束符也要用/,否则启动会失败;
  2. cmd文件里的ZOOCFG=,不要写成ZOOCFG%=,否则也会启动失败;
  3. 这三个服务是是伪集群服务,当启动一个时,不免有报错信息,三个都启动就恢复正常。
  4. Zookeeper节点数必须是奇数,所以这里创建了最少的节点数,选出其中的一个为leader也就是主节点。

第二部分SpringBoot和Zookeeper的集成
首先,在pom中引入一个必须的架包;


            org.springframework
            spring-web
        
        
        
            org.apache.zookeeper
            zookeeper
            3.5.5
        

第二步,创建服务注册的配置文件;

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 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 nodeList = zk.getChildren(nodePath,new Watcher() { 
                @Override 
                public void process(WatchedEvent event) {
                    if (event.getType() == Event.EventType.NodeChildrenChanged) {
                        // 发生子节点变化时再次调用此方法更新服务地址 
                        watchNode(zk); 
                    }
                } 
            }); 
            List 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及端口号地址;

SpringBoot集成Zookeeper,实现服务的注册与发现踩坑_第1张图片
启动服务,注册服务

SpringBoot集成Zookeeper,实现服务的注册与发现踩坑_第2张图片
发现并返回服务

在这里多启动了几个端口,从服务发现里可以看到,启动的端口都被注册到了zookeeper服务器上,这样就可以根据返回的ip+port进行调用具体的服务。

你可能感兴趣的:(SpringBoot集成Zookeeper,实现服务的注册与发现踩坑)