通常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即为要抢注的节点