使用zookeeper来解决在分布式系统中单节点维护微信token生命周期的容灾demo【已抽象分离】[分布式锁][9.28更新]
来源:互联网 发布:淘宝客cms系统 编辑:程序博客网 时间:2024/05/16 12:06
该备注增加时间 - 2016.12.21
微信SDK自用封装: https://git.oschina.net/zhuqiang/wx_sdk
以下文章只是最早萌发的一个基础想法和实现,在上面的sdk里面已经实现得更好了。
目标
本文目标是为了解决:在开发微信公众号应用的时候,要面临的一个问题,维护基础支持的accessToken 的生命周期的问题,由于2小时过期,还有调用次数的限制,所以不得不专门来维护accessToken的生命周期(微信的toke刷新也是直接发起一个获取token,那么以前的token就不能被使用了)。 那么问题就来了,在分布式系统中,要解决的问题有两个:
- 单节点去更新维护token的生命周期,并且要提供容灾机制(如果这台机器挂了,那么要有另外的机器顶上)
- 数据的共享问题(一台机器更新,另外的机器只管获取)
前提:
- 微信的基础accessToken 2小时过期,但是有一个容错机制,2分钟内的时差都可以被正确执行。
- 不能对动态新增微信公众号的accessToken提供支持(也就是说:系统启动的时候必须要事先知道有哪些公众号要维护)
- 维护的公众号Token不能太多,太多的话,数据就不能放在zk上了。
思路:
- 使用zk的监听功能
- 所有新启动的机器,要先去 父目录下 新建子目录,类型是临时序列号目录
- 所有的机器同时要 监听 父目录 的数据 变化 和 父目录的 子节点列表的变化
- 如果列表有变化,则需要判断最小的子目录是否是自己? 如果是,判断下 自己的更新任务是否有启动,如果已经启动过了,则不做任何操作。(更新线程中 要判断过期的token,并更新到zk的父目录中,以便zk回调所有的监听者)
- 在监听 数据变化的时候,要主动获取变化后的数据,更新到本地项目中。
zk基础
ZooKeeper Watches
所有zookeeper的读操作——getData(), getChildren(), exists()——都可以设置一个watch。Zookeeper的watch的定义是:watch事件是一次性触发的,发送到客户端的。在监视的数据 发生变化时产生watch事件。以下三点是watch(事件)定义的关键点:
一次性触发:
当数据发生变化时,一个watch事件被发送给客户端。例如,如果一个客户端做了一次getData(“/znode1”, true)然后节点/znode1发生数据变化或删除,这个客户端将收到/znode1的watch事件。如果/znode1继续发生改变,不会再有watch发送,除非客户端又做了其他读操作产生了新的watch。发送给客户端:
这就意味着,事件在发往客户端的过程中,可能无法在修改操作成功的返回值到达客户端之前到达客户端。watch是异步发送给watchers的。 zookeeper提供一种保证顺序的方法:客户端在第一次看到某个watch事件之前不可能看到产生watch的修改的返回值。网络延时或其他因素可能 导致不同客户端看到watch并返回不同时间更新的返回值。关键的一点是,不同的客户端看到发生的一切都必须是按照相同顺序的。watch依附的数据:
这是说改变一个节点有不通方式。用好理解的话说,zookeeper维护两组watch:data watch和child watch。getData()和exists()产生data watch。getChildren()引起child watch。watch根据数据返回的种类不同而不同。getData()和exists()返回关于节点的数据信息,而getChildren()返回 子节点列表。因此setData()触发某个znode的data watch(假设事件成功)。create()成功会触发被创建的znode上的data watch和在它父节点上的child watch。delete()成功会触发data watch和child watch(因为没有了子节点)。watch在客户端已连接上的服务器里维护,这样可以保证watch轻量便于设置,维护和分发。当客户端连接了一台新的服务器,watch会在任何 session事件时触发。当断开和服务器的连接时,watch不会触发。当客户端重新连接上时,任何之前注册过的watch都会重新注册并在需要的时候 被触发。一般来说这一切都是透明的。只有一种可能会丢失watch:当一个znode在断开和服务器连接时被创建或删除,那么判断这个znode存在的 watch因未创建而找不到。
以上引用:http://my.oschina.net/sundiontheway/blog/346498#OSC_h2_6
总结下:
- 在创建zk链接的时候 会设置一个watch监听创建成功后的回调。
- 在 getData(), getChildren(), exists() 也可以设置一个watch监听
- 每次放置的watch只能被回调一次。所以需要递归的放置watch
- 当把一个watch对象同时放置给不同的调用操作的时候,有可能只会被触发其中一个。例如,如果一个watch对象同时用来监控某个目标文件是否存在和监听getData(),之后那个文件被删除了。那么这个watch对象只会触发一次文件删除事件通知
- zk的集群同步性很强,所以我们可以忽略一些并发的问题。几乎上,你做一个操作,所有的zk节点看到的数据都是一致的。
demo,示例
只是一个模拟,差不多大体的框架搭建好了,只要根据具体的业务逻辑 修改就成了。
运行测试类 能看到效果
pom.xml 依赖
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> <dependency> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> <version>3.4.6</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.7</version> </dependency>
log4j.properties
### 输出到控制台 ###log4j.logger.你需要打印信息的包名 = wxInfolog4j.appender.wxInfo=org.apache.log4j.ConsoleAppenderlog4j.appender.wxInfo.Target = System.outlog4j.appender.wxInfo.layout = org.apache.log4j.PatternLayoutlog4j.appender.wxInfo.layout.ConversionPattern =[wx.sdkLog] %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n
测试类
/** * @author zhuqiang * @version V1.0 * @Description: * @date 2015/9/23 21:01 */public class ZClientTest { // 启动1个测试,可以看到控制台输出的消息,然后 把001 强制结束,可以看到 任务被002 接管了 @Test public void t001() throws InterruptedException { Zk zk = new Zk(); while (true){ TimeUnit.SECONDS.sleep(5); } } @Test public void t002() throws InterruptedException { Zk zk = new Zk(); for (int i = 0; i < 100000; i++) { TimeUnit.SECONDS.sleep(10); } }}
zk api工具类
9.28 更新:改变了一些日志打印信息,配合日志显示,方便调试观看
/** * @author zhuqiang * @version V1.0 * @Description: 客户端列表 * @date 2015/9/23 19:32 */public class ZkClientUtil { private static Log log = new Log(LoggerFactory.getLogger(ZkClientUtil.class)); /** * 创建ZK连接 * * @param connectString ZK服务器地址列表 * @param sessionTimeout Session超时时间 * @param watcher 监视者 */ public static ZooKeeper createConnection(String connectString, int sessionTimeout,Watcher watcher) { ZooKeeper result = null; try { result = new ZooKeeper(connectString, sessionTimeout, watcher); } catch (IOException e) { e.printStackTrace(); String errMsg = "创建ZooKeeper 客户端失败:" + e.getMessage(); log.e(errMsg); throw new RuntimeException("zk 初始化失败:" + errMsg); } return result; } /** * 关闭ZK连接 */ public static void releaseConnection(ZooKeeper zk) { if (zk != null) { try { zk.close(); } catch (InterruptedException e) { // ignore e.printStackTrace(); } } } /** * * @param zk * @param path 节点path * @param data 初始数据内容 * @param createMode * @return */ public static String createPath(ZooKeeper zk,String path, String data, CreateMode createMode) { String result = null; try { result = zk.create(path, // data.getBytes("utf-8"), // ZooDefs.Ids.OPEN_ACL_UNSAFE, // createMode); log.i("节点:" + result + " 创建成功"); } catch (KeeperException e) { String errMsg = "节点创建失败,发生KeeperException:" + e.getMessage(); log.e(errMsg); throw new RuntimeException(errMsg); } catch (InterruptedException e) { String errMsg = "节点创建失败,发生InterruptedException:" + e.getMessage(); log.e(errMsg); throw new RuntimeException(errMsg); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return result; } /** * 读取指定节点数据内容 * * @param path 节点path * * @return */ public static String readData(ZooKeeper zk,String path) { try { byte[] data = zk.getData(path, false, null);// log.i("获取数据成功,path:" + path); try { return new String(data,"utf-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } catch (KeeperException e) { log.e("读取数据失败,发生KeeperException,path: " + path + ":" + e.getMessage()); e.printStackTrace(); return ""; } catch (InterruptedException e) { log.e("读取数据失败,InterruptedException,path: " + path + ":" + e.getMessage()); e.printStackTrace(); return ""; } return null; } /** * 判断是否存在该目录,不监视改变 * @param zk * @param path * @return 返回true 已经存在 */ public static boolean exists(ZooKeeper zk,String path){ try { Stat exists = zk.exists(path, false); return exists == null?false:true; } catch (KeeperException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } return false; } /** * 更新指定节点数据内容 * @param path 节点path * @param data 数据内容 * @return */ public static boolean writeData(ZooKeeper zk,String path, String data) { try { Stat stat = zk.setData(path, data.getBytes(), -1);// log.i("更新数据成功,path:" + path + " : stat= " + JSON.toJSONString(stat)); } catch ( KeeperException e ) { log.e( "更新数据失败,发生KeeperException,path: " + path ); e.printStackTrace(); } catch ( InterruptedException e ) { log.e( "更新数据失败,发生 InterruptedException,path: " + path ); e.printStackTrace(); } return false; }}
线程模拟 维护token类
/** * @author zhuqiang * @version V1.0 * @Description: 维护生命周期 * @date 2015/9/22 21:30 */public class AccessTokenDistributedTask implements Runnable{ private ZooKeeper zk = null; public AccessTokenDistributedTask(ZooKeeper zk) { this.zk = zk; } @Override public void run() { for (int i = 0; i < 100000000; i++) { try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } if( i % 8 == 0) { String s = ZkClientUtil.readData(zk, Zk.ROOT_PATH); System.out.println(s + " : 即将被更新:原因:因为快超时了"); ZkClientUtil.writeData(zk, Zk.ROOT_PATH, "token-" + i); System.out.println(s + " : 已被更新:新token是:" + "token-" + i); } } }}
主要核心类
/** * @author zhuqiang * @version V1.0 * @Description: * @date 2015/9/23 20:28 */public class Zk implements Watcher { private static Logger logger = LoggerFactory.getLogger(Zk.class); public static final int SESSION_TIMEOUT = 10000; public static final String CONNECTION_STRING = "192.168.184.145:2181,192.168.184.146:2181,192.168.184.147:2181"; public static final String ROOT_PATH = "/wxBaseToken"; private ZooKeeper zk = null; private static String ip; //用于生成子目录的前缀部分 private String thisPath; //记录创建的子目录path private CountDownLatch cdl = new CountDownLatch(1); //用于成功创建 zk链接后,本类才能被初始化完成。 private boolean isRunTask = false; //记录任务是否已被开启 public Zk() { try { String ip = InetAddress.getLocalHost().getHostAddress(); this.ip = ip + "_"; zk = ZkClientUtil.createConnection(CONNECTION_STRING, SESSION_TIMEOUT,this); cdl.await(); } catch (UnknownHostException e) { String errMsg = "获取ip地址错误:" + e.getMessage(); logger.error(errMsg); e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } boolean exists = ZkClientUtil.exists(zk, ROOT_PATH); if(!exists){ ZkClientUtil.createPath(zk,ROOT_PATH,"root",CreateMode.PERSISTENT); } //链接成功后,创建子目录(临时目录序列号:目录规则:ip_序列号) String thisPath = ZkClientUtil.createPath(zk,ROOT_PATH + "/" + ip, "child", CreateMode.EPHEMERAL_SEQUENTIAL); if (thisPath != null) { this.thisPath = thisPath; }else{ logger.error("初始化zk客户端服务失败"); throw new RuntimeException("初始化zk客户端服务失败"); } } /** * 达到循环监视的步骤: * 1. 在 每次有watche事件的时候,接收之后,再次放置watched * 2. 在 链接成功的时候:调用处理列表变化的操作。 * 3. 在列表变化的操作里面:获取列表的时候再次把把watched设置 * 4. 在最后 重新把watch监视器放置到 监视整个父目录的 创建 和 更改事件。 * 5. 这样一来,该process(watch 会收到两次事件推送:exists 和 getChildren) * // 6. 该方法运行完成之后,会被zk 受到上面两次事件的推送,再次进入到此方法,就照成了递归调用 * (通过测试可以看到,如果是列表改变,process 会被回调两次) -- 被分离出去了 * * 如果要放到 web 容器中,我觉得,还是要用一个线程来 启动该类,就如果在test中一样,空循环 * * 在客户端 获取 本处更新的 token的时候:处理流程可以使用阻塞式:一直获取,直到能在本地map中获取到为止,然后返回 * @param watchedEvent */ @Override public void process(WatchedEvent watchedEvent) { if (Event.KeeperState.SyncConnected == watchedEvent.getState()) { //应该是同步状态下? if(Event.EventType.None == watchedEvent.getType()){ System.out.println("k:链接zk成功:" + watchedEvent.getState() + "\n"); cdl.countDown(); System.out.println("k:首次取得数据:" + ZkClientUtil.readData(zk, ROOT_PATH)); this.handerList(); }/*else if(Event.EventType.NodeChildrenChanged == watchedEvent.getType()){ //NodeChildrenChanged 子节点列表改变 System.out.println("k:子节点列表改变了:************************************"); this.handerList(); }*/else if(Event.EventType.NodeDataChanged == watchedEvent.getType()){ //NodeDataChanged 该节点下的数据被改变 System.out.println("k:节点数据改变了:***********************************"); System.out.println("k:已把改变的数据刷新到了本地内存:" + ZkClientUtil.readData(zk, watchedEvent.getPath())); } try { zk.exists(ROOT_PATH,this); //当第一次 和 数据被改变的时候, 重新放置父目录的监视器 } catch (KeeperException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } } } /** * 看到 zk 中的 process 了没,注释掉了处理 子节点改变的 代码,把这个处理,专门写了一个watcher * 用来处理 子节点改变的动作。 */ class ListWatcher implements Watcher{ @Override public void process(WatchedEvent watchedEvent) { if(Event.EventType.NodeChildrenChanged == watchedEvent.getType()){ //NodeChildrenChanged 子节点列表改变 System.out.println("ListWatcher:子节点列表改变了:************************************"); handerList(); } } } private ListWatcher listWatcher = new ListWatcher(); /** * 处理 */ public void handerList(){ try { zk.exists(ROOT_PATH,this); //监视 父目录的改变。 如果这里不监视父目录的改变,那么 当目录中的数据改变的时候,没有process被回调 List<String> list = zk.getChildren(ROOT_PATH, listWatcher); // 获取子目录的时候放置一个 watcher String[] sequentials = new String[list.size()]; for (int i1 = 0; i1 < list.size(); i1++) { String[] split = list.get(i1).split("_"); sequentials[i1] = split[1]; } Arrays.sort(sequentials); String minSequential = sequentials[0]; String[] split = thisPath.split("_"); String thisSequential = split[1]; if (thisSequential.equals(minSequential)) { //如果自己是最小的序列,则执行更新操作 System.out.printf("%s是最小的序列:%s\n", Thread.currentThread().getName(), thisPath); //如果该自己做 leader 了,要先判断任务是否已经开启过了 if(!isRunTask){ System.out.println("任务已被:" + this.thisPath + "接管"); new Thread(new AccessTokenDistributedTask(zk)).start(); isRunTask = true; } } System.out.println("父目录有改变了"); } catch (KeeperException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } }}
运行部分结果:
- 启动t001 测试方法,随后启动 t002,过一会,手动终结t001 下面是t001 控制台打印的消息
log4j:WARN No appenders could be found for logger (org.apache.zookeeper.ZooKeeper).log4j:WARN Please initialize the log4j system properly.log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.k:链接zk成功:SyncConnectedk:首次取得数据:token-0ListWatcher:子节点列表改变了:************************************main-EventThread是最小的序列:/wxBaseToken/169.254.25.222_0000000019任务已被:/wxBaseToken/169.254.25.222_0000000019接管父目录有改变了ListWatcher:子节点列表改变了:************************************main-EventThread是最小的序列:/wxBaseToken/169.254.25.222_0000000019父目录有改变了token-0 : 即将被更新:原因:因为快超时了k:节点数据改变了:***********************************k:已把改变的数据刷新到了本地内存:token-0token-0 : 已被更新:新token是:token-0token-0 : 即将被更新:原因:因为快超时了k:节点数据改变了:***********************************token-0 : 已被更新:新token是:token-8k:已把改变的数据刷新到了本地内存:token-8Process finished with exit code -1
- t002 控制台打印的消息,在t001 被终结之后,可以看到 在超时后,更新任务被t002接管了
log4j:WARN No appenders could be found for logger (org.apache.zookeeper.ZooKeeper).log4j:WARN Please initialize the log4j system properly.log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.k:链接zk成功:SyncConnectedk:首次取得数据:token-0ListWatcher:子节点列表改变了:************************************父目录有改变了k:节点数据改变了:***********************************k:已把改变的数据刷新到了本地内存:token-0k:节点数据改变了:***********************************k:已把改变的数据刷新到了本地内存:token-8 ***------ 这里,在t001被终结之后,后面的是接管信息***ListWatcher:子节点列表改变了:************************************main-EventThread是最小的序列:/wxBaseToken/169.254.25.222_0000000020任务已被:/wxBaseToken/169.254.25.222_0000000020接管父目录有改变了token-8 : 即将被更新:原因:因为快超时了k:节点数据改变了:***********************************k:已把改变的数据刷新到了本地内存:token-0token-8 : 已被更新:新token是:token-0
核心类被改造成 抽象类,继承该抽象类,实现抽象方法实现业务逻辑即可
把上面的核心类,给改造了一下。把实现 和业务逻辑给分离了出来。
忘记用 处理列表 分离之后的列子了。如有需要:自己动下手。简单参照上面分离之后的列子修改下吧。
9.24 : 更新和遇到的坑:
- firstRefresh 方法:
链接到zk 的时候 第一次取得数据,有可能为空.- 因为在继承本类的时候,要先初始化 父类,那么这个时候还未初始化完成,第一次数据将会被推送。
- 而 自定义的业务参数,在构造器中传入的。还没有被赋值,所以不能在该方法中使用 构造传入的值
- 在实现的时候注意的坑:
- 在准备 开启任务所需要的数据的时候,建议在 executor 方法中去准备,因为该方法只被回调一次,不要在 firstRefresh 中尝试,当然要根据业务使用场景 选择。
- 一定要注意你在任务中写入到zk上的数据结构,要处理好,不然在测试的时候,解析出错了,有可能连错误都看不到。 不知道是不是和zk的一些线程相关的东西有关。
- 在测试的时候,遇到的大坑。
- 出现在 main方法中,测试 能正常的得到期望的效果,但是放到web环境中,就出现直接阻塞,死锁的现象,结果搞了很久,才发现: 是传入的zk 服务器地址端口号写错了。 测试中,和 web环境中的服务器端口号不一致。
9.28 :更新和修复一些线程回调的空指针bug
- 获取ip 和第一次处理列表的时候,ip可能还未获取,就执行了处理列表,空指针异常[已解决]
- 写实现类的时候:
- firstRefresh 方法推送的数据,需要更新到 本地中(在上面9.24中第1条说,未初始化的问题,不知道怎么在这一个更新版本中,神奇的不出现了: 原因可能是有一个时间差:因为我debug的时候,查看自己自定义在构造中传入的参数还是空,过了一会就被赋值上了,表示父类已经初始化完成了,或许保险一点的解决方案是:在实现类中,firstRefresh 中需要先休眠几秒)
- 为什么要在firstRefresh 把获取的到数据更新到本地呢,是因为:在executor的时候才准备任务线程更新的参数,那么如果不是自己更新任务呢,那么本地就会有一段时间没有可用数据。
- 还要考虑到一个问题:如果第一次启动就是自己是leader呢,那么启动线程和任务就有一个时间差。也是需要在实现的时候考虑到的。 总之,第一次把分布式用到工作中,感觉还是有很多问题需要考虑的
/** * 分布式系统中,自动维护 选举单节点 任务更新 的 抽象类 * @author zhuqiang * @version V1.0 * @Description: * @date 2015/9/23 23:01 */public abstract class ZkDistributedSingleNodeExecutor implements Watcher { private static Log log = new Log(LoggerFactory.getLogger(ZkDistributedSingleNodeExecutor.class)); public int sessionTimeout; public String zkServiceList; public String rootPath; private ZooKeeper zkClient = null; private String ip; private String thisPath; private CountDownLatch cdl = new CountDownLatch(1); private CountDownLatch cdlIp = new CountDownLatch(1); private boolean isRunTask = false; //记录任务是否已被开启 /** * 构建 执行器 * @param sessionTimeout 链接超时毫秒 * @param zkServiceList zk服务器地址,逗号隔开 * @param rootPath 根目录 路径(不存在将被自动创建,最好存在) 例如: /wxBaseToken */ public ZkDistributedSingleNodeExecutor(int sessionTimeout, String zkServiceList,String rootPath) { this.sessionTimeout = sessionTimeout; this.zkServiceList = zkServiceList; this.rootPath = rootPath; if(sessionTimeout <= 0 || zkServiceList == null || rootPath == null){ throw new IllegalArgumentException("所有构造参数都不能为空,int 类型不能小于0"); } init(); } private void init() { try { String ip = InetAddress.getLocalHost().getHostAddress(); /* if(StringUtils.isBlank(ip)){ String errMsg = "初始化 " + this.getClass().getName() + " 时,未获取到ip地址,请重试!"; log.e(errMsg); throw new RuntimeException(errMsg); }*/ this.ip = ip + "_"; zkClient = ZkClientUtil.createConnection(zkServiceList, sessionTimeout, this); cdl.await(); } catch (UnknownHostException e) { String errMsg = "获取ip地址错误:" + e.getMessage(); log.e(errMsg); e.printStackTrace(); throw new RuntimeException(e); } catch (InterruptedException e) { e.printStackTrace(); throw new RuntimeException(e); } isExistsAndCreateRootPath(); //链接成功后,创建子目录(临时目录序列号:目录规则:ip_序列号) String thisPath = ZkClientUtil.createPath(zkClient, this.rootPath + "/" + ip, "child", CreateMode.EPHEMERAL_SEQUENTIAL); if (thisPath != null) { this.thisPath = thisPath; log.i(rootPath + ":新增了子目录:" +thisPath); cdlIp.countDown(); }else{ String errMsg = rootPath + ":初始化zk客户端服务失败"; log.e(errMsg); throw new RuntimeException(errMsg); } } private void isExistsAndCreateRootPath() { try { boolean exists = ZkClientUtil.exists(zkClient, this.rootPath); if(!exists){ JSONObject obj = new JSONObject(); obj.put("info","true"); ZkClientUtil.createPath(zkClient, this.rootPath,obj.toJSONString(), CreateMode.PERSISTENT); } }catch (RuntimeException e){ log.e("创建:根目录:" + rootPath + "出现了一个致命的错误,如果您的项目是第一次使用该zookpeer服务器时启动,我们建议您,重新再次启动一次项目 或则 确保 zookpeer上存在该目录"); e.printStackTrace(); } } /** * 用与 监听 链接成功,和 根目录 数据改变 需要做的操作 * @param watchedEvent */ @Override public void process(WatchedEvent watchedEvent) { if (Event.KeeperState.SyncConnected == watchedEvent.getState()) { //应该是同步状态下? if(Event.EventType.None == watchedEvent.getType()){ log.d("\n" + rootPath + " :链接zk成功:" + watchedEvent.getState() + "\n"); cdl.countDown(); isExistsAndCreateRootPath(); String data = ZkClientUtil.readData(zkClient, rootPath); firstRefresh(data); this.handerList(); }else if(Event.EventType.NodeDataChanged == watchedEvent.getType()){ //NodeDataChanged 该节点下的数据被改变 log.d( rootPath + "-->" + thisPath + ":节点数据改变了:***********************************"); refresh(ZkClientUtil.readData(zkClient, watchedEvent.getPath())); } try { zkClient.exists(rootPath, this); } catch (KeeperException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } } } class ListWatcher implements Watcher{ @Override public void process(WatchedEvent watchedEvent) { if(Event.EventType.NodeChildrenChanged == watchedEvent.getType()){ //NodeChildrenChanged 子节点列表改变 log.i(rootPath + "-->" + thisPath + ":子节点列表改变了:************************************"); handerList(); } } } private ListWatcher listWatcher = new ListWatcher(); /** * 子节点列表改变,需要判断 自己是否需要启动更新任务。 */ private void handerList(){ try { if(thisPath == null){ cdlIp.await(); } zkClient.exists(rootPath,this); List<String> list = zkClient.getChildren(rootPath, listWatcher); if(list == null || list.size() <= 0){ return; } String[] sequentials = new String[list.size()]; for (int i1 = 0; i1 < list.size(); i1++) { String[] split = list.get(i1).split("_"); sequentials[i1] = split[1]; } Arrays.sort(sequentials); String minSequential = sequentials[0]; String[] split = thisPath.split("_"); String thisSequential = split[1]; if (thisSequential.equals(minSequential)) { //如果自己是最小的序列,则执行更新操作 log.d(rootPath + "-->" + thisPath + ":最小的序列是:" + thisPath); //如果该自己做 leader 了,要先判断任务是否已经开启过了 if(!isRunTask){ log.d(rootPath + "-->" + thisPath + ":任务已被:" + this.thisPath + "接管"); executor(this,ZkClientUtil.readData(zkClient, rootPath)); isRunTask = true; } } log.d(rootPath + "-->" + thisPath + ":父目录: " + rootPath + "有改变了~"); } catch (KeeperException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } } /** 往 zk 上更新数据 */ public void writeData(String data){ ZkClientUtil.writeData(zkClient, rootPath,data); } /** * 需要执行任务的回调方法, 该方法,只会被回调一次,所以请 在该方法内保证你的任务 会被正常启动 * @param zkDistributedSingleNodeExecutor * @param data 该自己执行任务的时候,会获取一次数据推送给本方法 */ protected abstract void executor(ZkDistributedSingleNodeExecutor zkDistributedSingleNodeExecutor,String data); /** * 链接到zk 的时候 第一次取得数据,有可能为 创建父目录写入的固定数据. * 因为在继承本类的时候,要先初始化 父类,那么这个时候还未初始化完成,第一次数据将会被推送。 * 而 自定义的业务参数,在构造器中传入的。还没有被赋值,所以不能在该方法中使用 构造传入的值 * 由于业务实现问题:第一次接收到的数据,或许是其他机器上传的,所以应该被解析更新到本地。 * (这是一个坑,因为微信的jssdk授权的ticket,需要依赖 基础支持中的token,如果在这里不更新,那么就会出现获取不到token的情况) * @param data */ protected abstract void firstRefresh(String data); /** * 刷新数据,每次有数据改变的时候,将会通过次方法 推送,数据一般为json类型,(具体是什么结果,和你在任务中写入的结构一致) * @param data */ protected abstract void refresh(String data);}
结束
不熟悉分布式的东西,临时抱佛教,得到一些朋友的指点和百度的搜索。 也只能了解一点皮毛。
比较不错的入门资料:ZooKeeper开发手册中文翻译
Zookeeper Api(java)入门与应用(转)
- 使用zookeeper来解决在分布式系统中单节点维护微信token生命周期的容灾demo【已抽象分离】[分布式锁][9.28更新]
- zookeeper分布式锁DEMO
- zookeeper来完成分布式锁的功能
- 基于单Redis节点的分布式锁
- ZooKeeper使用--分布式锁
- 分布式系统笔记:利用zookeeper实现分布式leader节点选举
- 使用zookeeper一个简单分布式demo
- 安装一个单节点的 Hadoop 分布式系统
- Zookeeper实现分布式应用的(主节点HA)及客户端动态更新主节点状态
- 使用redis实现分布式锁,可以解决集群中需要单例的情况
- Zookeeper解决分布式锁问题
- 分布式消息系统kafka单节点搭建
- zookeeper的分布式锁
- FastDFS分布式文件系统的安装与使用(单节点)
- FastDFS 分布式文件系统的安装与使用 (单节点)
- FastDFS 分布式文件系统的安装与使用(单节点)
- zookeeper单节点安装和伪分布式集群安装
- Zookeeper在大型分布式系统中的应用
- 数据库插入查询练习之词典
- 注册表Regedit 笔记
- 这家获得谷歌投资的企业想把亚马逊挑下神坛
- ubuntu 添加 listings 宏包
- 访问类的私有成员
- 使用zookeeper来解决在分布式系统中单节点维护微信token生命周期的容灾demo【已抽象分离】[分布式锁][9.28更新]
- 【FPGA黑金开发板】Verilog HDL那些事儿--串口模块(十一)
- NYOJ 518 取球游戏(博弈问题)
- MySql数据库,对varchar类型字段str进行where str=0条件查询时,查询结果是什么
- 33. BOM
- Eclipse JAVA文件注释乱码
- PHP基础篇-表单完整实例
- linux(知识)
- 【UIKit-124-7】#import <UIKit/UIView.h>