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中引入一个必须的架包;

<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 ({} =&gt; {} =&gt; {})", 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进行调用具体的服务。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容