Zookeeper第二话 -- Springboot基于zk watch机制实现公用配置和备用节点

1.起因

在程序员的世界,当然是希望服务一成不变的稳定,这样能极大的减少服务器分析日志的原因。但是,任何服务总会在某个时候出现高峰期,例如:618、双十一、十二等等。

那么当产品经理来通知我们程序员的时候,说某个时期段可能会有大流量注入,我们该怎么办呢?
我相信我们的回答是肯定的!
加机器加服务升配置!

那么问题就来了,有些服务在同一时间只需要指定个数的服务在运行,当服务数量少于约定个数时,其他服务补上,这时候该怎么做呢?
真实案例:根据长链接订阅远端发送过来的数据,数据量不定时波动,于是启动了备用机以保证服务的高可用

2.springboot + zk实现备用机

基于springboot2.5.6,zookeeper3.7

2.1 pom文件

zookeeper依赖

<dependency>
	<groupId>org.apache.zookeeper</groupId>
	<artifactId>zookeeper</artifactId>
	<version>3.7.0</version>
	<exclusions>
		<!--要排除zk包里面的日志,不然冲突会启动报错-->
		<exclusion>
			<artifactId>slf4j-log4j12</artifactId>
			<groupId>org.slf4j</groupId>
		</exclusion>
		<exclusion>
			<artifactId>log4j</artifactId>
			<groupId>log4j</groupId>
		</exclusion>
	</exclusions>
</dependency>

2.2 application-zk.yml文件

#备用机相当先过的zk,还没有交由spring管理,故此单独一份配置
zookeeper:
  address: 192.168.0.221:2181
  timeout: 60000
  #节点抢占锁的路径
  defPath: /zk.service.node
  #启动节点数
  nodeNum: 2

2.3 config初始化代码

@Slf4j
public class ZookeeperConfig {

    static String address = null;
    static String defPath;
    static int nodeNum;
    static int timeout;
    static Object object = new Object();
    static ZooKeeper zooKeeper;

	//没有交由spring管理 配置就自己读取
    private static void initConfig() throws IOException {
        if (address != null) return;
        InputStream stream = ZookeeperConfig.class.getClassLoader().getResourceAsStream("application-zk.yml");
        //这里读取yam文件 切勿出现重名的key不然会被覆盖为最后那个
        Properties properties = new Properties();
        properties.load(stream);
        address = properties.getProperty("address");
        defPath = properties.getProperty("defPath");
        nodeNum = Integer.valueOf(properties.getProperty("nodeNum"));
        timeout = Integer.valueOf(properties.getProperty("timeout"));
    }

	//main方法入口
    public static void init(){
        try {
        	//初始化配置
            initConfig();
            //初始化zk 初始化时不添加监听
            zooKeeper = new ZooKeeper(address, 60000,
                    watchedEvent -> {
                    });
            //死循环里面 也可以设置指定重试次数        
            while (true) {
            	//加锁是为了 监听时唤醒
                synchronized (object) {
                	//开始抢占节点 在path后面加数字
                    for (int i = 0; i < nodeNum; i++) {
                        String path = defPath + i;
                        try {
                        	//zk的创建api 创建一个临时节点 服务关闭 40秒后会自动删除
                            String s = zooKeeper.create(path, "1".getBytes(),
                                    ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
                            //创建成果则取消监听
                            zooKeeper.exists(path, false);
                            log.info("创建节点成功:{}", s);
                            return;
                        } catch (KeeperException ex) {
                            log.error("节点已存在,添加监听并等待节点事件:{}", path);
                            addZkWatch(path, zooKeeper);
                        } catch (Exception e) {
                            log.error("执行异常:", e);
                            throw new RuntimeException("zk初始化失败");
                        }
                    }
                    //释放锁等待需要在for循环外 以便尝试每个节点的抢占
                    object.wait();
                }
            }
        } catch (Exception e) {
            log.error("初始化zk异常!");
            throw new RuntimeException(e);
        }
    }


    private static void addZkWatch(String path, ZooKeeper zooKeeper) {
        log.info("添加监听:{}", path);
        try {
        	//添加path的监听
            zooKeeper.exists(path, watchedEvent -> {
                synchronized (object) {
                    String eventPath = watchedEvent.getPath();
                    Watcher.Event.EventType eventType = watchedEvent.getType();
                    log.info("收到节点事件======{}", eventPath, eventType);
                    if (Watcher.Event.EventType.NodeDeleted.equals(eventType)) {
                        log.info("发现节点删除,唤醒服务抢占");
                        object.notifyAll(); //唤醒当前服务继续去抢占
                    } else {
                        //原生包由map存储事件只会调用一次,处理过事件之后需要重新监听
                        addZkWatch(path, zooKeeper);
                    }
                }
            });
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

关于只能监听一次的情况说明:
对使用了existf()方法对某个路径监听,在本次监听完后需再次调用exist()方法注册监听,上述内容就是用的这种使用,另外一种为addWatch方法设定AddWatchMode属性,可以不用再次添加监听

//addWatch 用法
zooKeeper.addWatch("/yaml", watchedEvent -> {
                //something

            }, AddWatchMode.PERSISTENT_RECURSIVE);

2.3 main方法启动

public static void main(String[] args) {
	//先确定抢占到锁 再启动springboot服务
    ZookeeperConfig.init();
    SpringApplication.run(ZookeeperApplication.class, args);
}

2.4 日志输出

分别使用不同端口启动两个服务,查看日志

//服务1抢占成功
com.example.config.ZookeeperConfig - 创建节点成功:/zk.service.node0

//服务2抢占失败 则添加监听 接下来停掉服务1
com.example.config.ZookeeperConfig - 节点已存在,添加监听并等待节点事件:/zk.service.node0
com.example.config.ZookeeperConfig - 添加监听:/zk.service.node0
//在停掉第一个服务40秒后 服务2收到监听
com.example.config.ZookeeperConfig - 收到节点事件======/zk.service.node0
com.example.config.ZookeeperConfig - 发现节点删除,唤醒服务抢占
//服务2抢占成功
com.example.config.ZookeeperConfig - 创建节点成功:/zk.service.node0

如果多个服务只需要配置节点数即可,启动则依次去抢占node1,node2,nodeN等等

2.5 其他方式的实现

上述内容是直接通过原生包实现的,还有其他更好的实现,需要引入curator包

<dependency>
	<groupId>org.apache.curator</groupId>
	<artifactId>curator-framework</artifactId>
	<version>5.2.1</version>
</dependency>
<dependency>
	<groupId>org.apache.curator</groupId>
	<artifactId>curator-recipes</artifactId>
	<version>5.2.1</version>
</dependency>

main方法测试代码,大同小异喽

@Slf4j
public class CuratorTest {

    static String address = "192.168.0.221:2181";
    static String defPath = "/zk.service.node";
    static Object object = new Object();
    static TreeCache treeCache = null;

    public static void main(String[] args) throws Exception {
        init(2); //启动服务中只有2个正常运行 其他的备用
    }

    private static void init(int num) throws Exception {
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000, 3);
        CuratorFramework client = CuratorFrameworkFactory.builder()
                .connectString(address)
                .sessionTimeoutMs(60000)
                .connectionTimeoutMs(10000)
                .retryPolicy(retryPolicy).build();
        client.start();

        while (true) {
            synchronized (object) {
                for (int i = 0; i < num; i++) {
                    String path = defPath + i;
                    try {
                        String s = client.create().withMode(CreateMode.EPHEMERAL)
                                .withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)
                                .forPath(path, "1".getBytes());
                        //取消监听
                        if (treeCache != null) {
                            treeCache.close();
                        }
                        log.info("创建节点成功:{}", s);
                        return;
                    } catch (KeeperException ex) {
                        log.error("节点已存在,添加监听并等待节点事件:{}", path);
                        addZkWatch(path, client);
                    } catch (Exception e) {
                        log.error("执行异常:", e);
                        throw new RuntimeException("zk初始化失败");
                    }
                }
                object.wait();
            }
        }
    }


    private static void addZkWatch(String path, CuratorFramework client) {
        log.info("添加监听:{}", path);
        try {
            treeCache = TreeCache.newBuilder(client, path).build();
            treeCache.start();
            treeCache.getListenable().addListener((curatorFramework, treeCacheEvent) -> {
                switch (treeCacheEvent.getType()) {
                    case NODE_REMOVED:
                        log.info("收到节点事件======{},{}", treeCacheEvent.getData().getPath(), treeCacheEvent.getType());
                        synchronized (object) {
                            log.info("发现节点删除,唤醒服务抢占");
                            treeCache.close();
                            object.notifyAll();
                        }
                        break;
                    default:
                        log.info("=========={},{}", treeCacheEvent.toString());
                        break;
                }
            });
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

3.springboot + zk 实现公用配置管理

基于上述的代码继续改造,启动springboot服务后对其添加配置文件目录的监听

3.1 添加启动的配置类

@Slf4j
@Component
public class BeanConfig {

    @PostConstruct
    public void zkWatch() {
        try {
        	//我这里直接视为单个配置文件了 实际用的时候 应该是对分类key做监听
            ZookeeperConfig.zooKeeper.addWatch("/zk.config", watchedEvent -> {
                //other something
                log.info("收到监听事件:{}", watchedEvent.getType());
                if (Watcher.Event.EventType.NodeDataChanged.equals(watchedEvent.getType())) {
                    try {
                        byte[] data = ZookeeperConfig.zooKeeper.getData("/zk.config", false, null);
                        if (data != null && data.length > 0) {
                            //配置更新
                            log.info("配置更新:{}", new String(data));
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }, AddWatchMode.PERSISTENT_RECURSIVE);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("初始化zookeeper异常");
        }
    }
}

3.2 controller方法

@GetMapping("/set")
public String set(@RequestParam String path, @RequestParam String value) throws KeeperException, InterruptedException {
	//如果目录不存在 则先创建 偷懒的用法
    Stat stat1 = ZookeeperConfig.zooKeeper.exists(path, false);
    if (stat1 == null) {
        ZookeeperConfig.zooKeeper.create(path, value.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
    }
    Stat stat = ZookeeperConfig.zooKeeper.setData(path, value.getBytes(), -1);
    return stat.toString();
}

3.3 更新值测试日志

调用接口更新

com.example.config.BeanConfig            : 收到监听事件:NodeCreated
com.example.config.BeanConfig            : 收到监听事件:NodeDataChanged
com.example.config.BeanConfig            : 配置更新:1234567
com.example.config.BeanConfig            : 收到监听事件:NodeDataChanged
com.example.config.BeanConfig            : 配置更新:123456789

4.集群配置

zk的集群连接相对比较容易,只需要配置在地址上即可

zookeeper:
  address: 192.168.0.221:2181,192.168.0.222:2181

鉴于zk的监听还可以实现很多的功能,本内容为测试zk写出,这里仅供参考。
下节分析zk的集群搭建。
以上就是本章的全部内容了。

上一篇:Zookeeper第一话 – docker安装zookeeper以及Springboot集成zk
下一篇:Zookeeper第三话 – Springboot基于zk+dubbo实现远程服务调用

若要功夫深,铁杵磨成针

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值