通常zookeeper在分布式服务中作为注册中心,实际上它还可以办到很多事。比如分布式队列、分布式锁

由于公司服务中有很多定时任务,而这些定时任务由于一些历史原因暂时不能改造成框架调用

于是想到用zookeeper特性来实现

首先我们先了解下zk工作原理

结构图解释:左侧树状结构为zookeeper集群,右侧为程序服务器。所有的服务器在启动的时候,都会订阅zookeeper中master节点的删除事件,以便在主服务器挂掉的时候进行抢主操作;所有服务器同时会在servers节点下注册一个临时节点(保存自己的基本信息),以便于应用程序读取当前可用的服务器列表。

选主原理介绍:zookeeper的节点有两种类型,持久节点跟临时节点。临时节点有个特性,就是如果注册这个节点的机器失去连接(通常是宕机),那么这个节点会被zookeeper删除。选主过程就是利用这个特性,在服务器启动的时候,去zookeeper特定的一个目录下注册一个临时节点(这个节点作为master,谁注册了这个节点谁就是master),注册的时候,如果发现该节点已经存在,则说明已经有别的服务器注册了(也就是有别的服务器已经抢主成功),那么当前服务器只能放弃抢主,作为从机存在。同时,抢主失败的当前服务器需要订阅该临时节点的删除事件,以便该节点删除时(也就是注册该节点的服务器宕机了或者网络断了之类的)进行再次抢主操作。从机具体需要去哪里注册服务器列表的临时节点,节点保存什么信息,根据具体的业务不同自行约定。选主的过程,其实就是简单的争抢在zookeeper注册临时节点的操作,谁注册了约定的临时节点,谁就是master。

到此我们就可以着手实现了

  1 public class ZkMasterChooseUtil {
  2 
  3     public static final Map<String,ZkClient > map = new ConcurrentHashMap<>();
  4     public static final Map<String,String > pathMap = new ConcurrentHashMap<>();
  5     public static final Map<String,List<String>> childMap = new ConcurrentHashMap<>();
  6 
  7     public static final String IP = getServerIp();
  8 
  9     public static final boolean isMaster(String zkServer,String path){
 10 
 11         try {
 12             if (!map.containsKey(zkServer)){
 13                 reconnect(zkServer);
 14             }
 15         }catch (Exception e){
 16             reconnect(zkServer);
 17         }
 18         String seq = null;
 19         if (!pathMap.containsKey(path)){
 20             if (!map.get(zkServer).exists(path)){
 21                 map.get(zkServer).createPersistent(path,true);
 22             }
 23             map.get(zkServer).subscribeChildChanges(path, (parentPath, currentChilds) -> {
 24                 childMap.put(parentPath,resetList(currentChilds != null? currentChilds : new ArrayList<>()));
 25             });
 26             pathMap.remove(path);
 27             seq = map.get(zkServer).createEphemeralSequential(path+"/",IP);
 28             pathMap.put(path,seq);
 29             List<String> list = map.get(zkServer).getChildren(path);
 30             childMap.put(path,resetList(list != null ? list : new ArrayList<>()));
 31         }
 32         seq = pathMap.get(path);
 33         List<String>  list = childMap.get(path);
 34         if(list.size()>0){
 35             if ((path+"/"+list.get(0)).equals(seq)){
 36                 return true;
 37             }
 38             System.out.println("path = "+(path+"/"+list.get(0)) +"    seq = "+seq);
 39         }
 40         return false;
 41     }
 42 
 43     private static void reconnect(String zkServer){
 44         ZkClient zkClient = new ZkClient(new ZkConnection(zkServer,10000),10000);
 45         map.put(zkServer,zkClient);
 46     }
 47 
 48 
 49 
 50     private static String getServerIp() {
 51         try {
 52             InetAddress i = getLocalHostLANAddress();
 53             return i != null ? i.getHostAddress() : null;
 54         } catch (Exception e) {
 55             e.printStackTrace();
 56         }
 57         return "";
 58     }
 59 
 60     public static InetAddress getLocalHostLANAddress() {
 61         try {
 62             InetAddress candidateAddress = null;
 63             // 遍历所有的网络接口
 64             for (Enumeration ifaces = NetworkInterface.getNetworkInterfaces(); ifaces.hasMoreElements(); ) {
 65                 NetworkInterface iface = (NetworkInterface) ifaces.nextElement();
 66                 // 在所有的接口下再遍历IP
 67                 for (Enumeration inetAddrs = iface.getInetAddresses(); inetAddrs.hasMoreElements(); ) {
 68                     InetAddress inetAddr = (InetAddress) inetAddrs.nextElement();
 69                     // 排除loopback类型地址
 70                     if (!inetAddr.isLoopbackAddress()) {
 71                         if (inetAddr.isSiteLocalAddress()) {
 72                             // 如果是site-local地址,就是它了
 73                             return inetAddr;
 74                         } else if (candidateAddress == null) {
 75                             // site-local类型的地址未被发现,先记录候选地址
 76                             candidateAddress = inetAddr;
 77                         }
 78                     }
 79                 }
 80             }
 81             if (candidateAddress != null) {
 82                 return candidateAddress;
 83             }
 84             // 如果没有发现 non-loopback地址.只能用最次选的方案
 85             InetAddress jdkSuppliedAddress = InetAddress.getLocalHost();
 86             return jdkSuppliedAddress;
 87         } catch (Exception e) {
 88             e.printStackTrace();
 89         }
 90         return null;
 91     }
 92 
 93     private static List<String> resetList(List<String> list){
 94         if (list.size() == 0){
 95             return list;
 96         }
 97         Collections.sort(list, new Comparator<String>() {
 98             @Override
 99             public int compare(String o1, String o2) {
100                 Long l1 = Long.valueOf(o1.substring(o1.lastIndexOf("/")+1));
101                 Long l2 = Long.valueOf(o2.substring(o2.lastIndexOf("/")+1));
102                 return l1.compareTo(l2);
103             }
104         });
105         return list;
106     }
107 
108 }

注:传入path即为要抢注的节点