zookeeper选主

在多线程的web应用程序中,有时候同一时刻只允许一台服务器做某些操作,比如电商网站的库存加减,下单操作等,实现这样的业务,方法很多,一种是利用redis的setnx+expire实现(或者现在更成熟的redisson),一种是利用zk选主,让主服务器做这件事,其他服务器不操作(适合中小型应用,性能受限于单台机器,但中小企业足以应付),客户端调用方把所有需要主节点处理的请求全部转发到主节点上来。

下面主要是讲一下如何用java代码实现zookeeper选主。

1、zk选主原理

zk中的节点基本上有四种类型:

临时节点节点创建后,一旦服务器宕机或者客户端失去连接超过一定时间(可以配置),节点会被删除

临时有序节点当节点创建后,一旦服务器重启或者宕机,节点会被删除,与临时节点不同的是,它的每个节点都会默认存在节点序号,每个节点的需要都是有序递增的,比如a001,a002

持久节点: 一旦创建则永久存在于zk中,除非手动删除

持久有序节点:一旦创建永久存在,且每个节点都是有序的

临时节点同一时间点只允许一个线程创建,创建的时候必须抢占zk的共享锁,抢到了就可以创建,没抢到就没法创建,利用这一zk天然的特性,我们可以用来选主,谁抢到了锁并且成功创建了某个临时节点,我们就让他成为主节点。(ps:如果后抢到的线程需要做其他操作时,可以考虑临时有序节点)

2、基本思路如下:

a)、每台服务器都可以同时去zk上创建某个临时节点

b)、创建节点成功的机器为主服务器,我们将该机器的ip写入zk的该临时节点中,其他服务器皆为从服务器

c)、每台服务器在选主失败后,监听该临时节点的变更,一旦节点被删除,则触发再次选主,如果觉得监听节点删除后再重新选主比较麻烦,也可以自己再创建一个zk节点,该节点为永久节点,只更新数据,不删除,选主成功后往该节点再写一份数据,客户端直接监听该节点的数据变更即可(目前我们公司就用的这种方式)

3、实现代码

3.1)、zookeeper上创建节点

create /javaice "for select master"
create /javaice/zk_master_tmp ""

3.2)、依赖引入

gradle:

compile group: 'org.apache.zookeeper', name: 'zookeeper', version: '3.4.9'

maven:

<dependency>
         <groupId>org.apache.zookeeper</groupId>
         <artifactId>zookeeper</artifactId>
         <version>3.4.9</version>
</dependency>

3.3)、节点选主代码

import java.io.IOException;
import java.io.UnsupportedEncodingException;

import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
 
/**
 * 表示集群中的一个节点
 * @author le.zhou
 */
public class Node {
    private String nodeForLeaderInfo;
    private ZooKeeper zooKeeper;
 
    public Node(String listenerNodeForLeader) throws IOException {
        this.nodeForLeaderInfo = listenerNodeForLeader;
        
        ZkWatcher watcher = new ZkWatcher();

        String zkConnectionStr = "192.168.182.129:2181,192.168.182.130:2181,192.168.182.131:2181";
        this.zooKeeper = new ZooKeeper(zkConnectionStr, 5000, watcher);
        
        lookingForLeader();
    }
 
    public void lookingForLeader() {
        try {
        	//获取机器的ip和端口(生产环境可以改成ip和端口)
        	String hostAddr = Thread.currentThread().getName();
            // 需要注意这里创建的是临时节点
            zooKeeper.create(nodeForLeaderInfo, hostAddr.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
            // 如果上一步没有抛异常,说明自己已经是leader了
            System.out.println(Thread.currentThread().getName() + " is leader");
            //临时节点名称
            String nodeName = "/javaice/zk_master";
            //单独写一个zknode,给客户端调用放用
            zooKeeper.setData(nodeName, hostAddr.getBytes("utf8"), -1);
        } catch (KeeperException.NodeExistsException e) {
            //报NodeExistsException异常时, 说明节点已经存在,leader已经被别人注册成功了,自己是follower
            try {
                byte[] leaderInfoBytes = zooKeeper.getData(nodeForLeaderInfo, event -> {
                    if (event.getType() == Watcher.Event.EventType.NodeDeleted) {//监控主节点,主节点挂掉之后开始重新选主
                    	System.out.println("master is down, begin election...");
                        lookingForLeader();
                    }
                }, null);
                
                System.out.println(Thread.currentThread().getName() + " is follower, master is " + new String(leaderInfoBytes, "UTF-8"));
            } catch (KeeperException.NoNodeException e1) {
                // 如果在获取leader信息的时候报了节点不存在,说明这个leader比较短命,刚抢到leader就又挂掉了
                lookingForLeader();
            } catch (KeeperException | InterruptedException | UnsupportedEncodingException e1) {
                e1.printStackTrace();
            }
        } catch (KeeperException | InterruptedException e) {
        } catch (Exception e) {
        }
    }
}

3.4)、zk监听器

import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.Watcher.Event.EventType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ZkWatcher implements Watcher {
	private Logger logger = LoggerFactory.getLogger(this.getClass());
	
	@Override
	public void process(WatchedEvent event) {
		if(null==event) {
			return;
		}
		EventType eventTpe = event.getType();
		logger.info("eventType:"+eventTpe.name()+", path:"+event.getPath());
	}
}

3.5)、模拟3台客户端机器测试选主

模拟机器1:

import java.util.concurrent.TimeUnit;

/**
 * 模拟客户端机器
 * @author royise
 */
public class Client1 {
	public static void main(String[] args) {
		//临时节点名称
    	final String nodeName = "/javaice/zk_master_tmp";
		
		new Thread(() -> {
            try {
                new Node(nodeName);
                while (true) {
                    TimeUnit.SECONDS.sleep(1);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            //给线程加个名字,方便区分线程
        }, "zookeeper1").start();
	}
}

模拟机器2:

import java.util.concurrent.TimeUnit;

/**
 * 模拟客户端机器
 * @author royise
 */
public class Client2 {
	public static void main(String[] args) {
		//临时节点名称
    	final String nodeName = "/javaice/zk_master_tmp";
		
		new Thread(() -> {
            try {
                new Node(nodeName);
                while (true) {
                    TimeUnit.SECONDS.sleep(1);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            //给线程加个名字,方便区分线程
        }, "zookeeper2").start();
	}
}

模拟机器3:

import java.util.concurrent.TimeUnit;

/**
 * 模拟客户端机器
 * @author royise
 */
public class Client3 {
	public static void main(String[] args) {
		//临时节点名称
    	final String nodeName = "/javaice/zk_master_tmp";
		
		new Thread(() -> {
            try {
                new Node(nodeName);
                while (true) {
                    TimeUnit.SECONDS.sleep(1);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            //给线程加个名字,方便区分线程
        }, "zookeeper3").start();
	}
}

然后启动zookeeper集群,如何搭建zookeeper集群请参考我前面的zookeeper入门文章:https://blog.csdn.net/zhoulenihao/article/details/100670267

再分别启动上面的三个客户端类Client1.java、Client2.java、Client3.java

第一个窗口会看到如下信息:
zookeeper1 is leader

第二个窗口会看到如下信息:

zookeeper2 is follower, master is zookeeper1

第三个窗口会看到如下信息:

zookeeper3 is follower, master is zookeeper1

 

由上可知,当前主节点是zookeeper1,查看zookeeper最后弄过/javaice/zk_master_tmp节点的内容:

接着我们可以把第一个窗口关掉,模拟主节点宕机,这样会看到其他两个节点中有一个重新被选为主节点

zookeeper2 is follower, master is zookeeper1
master is down, begin election...
zookeeper2-EventThread is leader

 

 

 

 

 

 

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值