在多线程的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