使用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就不能被使用了)。 那么问题就来了,在分布式系统中,要解决的问题有两个:

  1. 单节点去更新维护token的生命周期,并且要提供容灾机制(如果这台机器挂了,那么要有另外的机器顶上)
  2. 数据的共享问题(一台机器更新,另外的机器只管获取)

前提:

  1. 微信的基础accessToken 2小时过期,但是有一个容错机制,2分钟内的时差都可以被正确执行。
  2. 不能对动态新增微信公众号的accessToken提供支持(也就是说:系统启动的时候必须要事先知道有哪些公众号要维护)
  3. 维护的公众号Token不能太多,太多的话,数据就不能放在zk上了。

思路:

  1. 使用zk的监听功能
  2. 所有新启动的机器,要先去 父目录下 新建子目录,类型是临时序列号目录
  3. 所有的机器同时要 监听 父目录 的数据 变化 和 父目录的 子节点列表的变化
  4. 如果列表有变化,则需要判断最小的子目录是否是自己? 如果是,判断下 自己的更新任务是否有启动,如果已经启动过了,则不做任何操作。(更新线程中 要判断过期的token,并更新到zk的父目录中,以便zk回调所有的监听者)
  5. 在监听 数据变化的时候,要主动获取变化后的数据,更新到本地项目中。

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

总结下:

  1. 在创建zk链接的时候 会设置一个watch监听创建成功后的回调。
  2. 在 getData(), getChildren(), exists() 也可以设置一个watch监听
  3. 每次放置的watch只能被回调一次。所以需要递归的放置watch
  4. 当把一个watch对象同时放置给不同的调用操作的时候,有可能只会被触发其中一个。例如,如果一个watch对象同时用来监控某个目标文件是否存在和监听getData(),之后那个文件被删除了。那么这个watch对象只会触发一次文件删除事件通知
  5. 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();        }    }}

运行部分结果:

  1. 启动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
  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 : 更新和遇到的坑:

  1. firstRefresh 方法:
    链接到zk 的时候 第一次取得数据,有可能为空.
    • 因为在继承本类的时候,要先初始化 父类,那么这个时候还未初始化完成,第一次数据将会被推送。
    • 而 自定义的业务参数,在构造器中传入的。还没有被赋值,所以不能在该方法中使用 构造传入的值
  2. 在实现的时候注意的坑:
    • 在准备 开启任务所需要的数据的时候,建议在 executor 方法中去准备,因为该方法只被回调一次,不要在 firstRefresh 中尝试,当然要根据业务使用场景 选择。
    • 一定要注意你在任务中写入到zk上的数据结构,要处理好,不然在测试的时候,解析出错了,有可能连错误都看不到。 不知道是不是和zk的一些线程相关的东西有关。
  3. 在测试的时候,遇到的大坑。
    • 出现在 main方法中,测试 能正常的得到期望的效果,但是放到web环境中,就出现直接阻塞,死锁的现象,结果搞了很久,才发现: 是传入的zk 服务器地址端口号写错了。 测试中,和 web环境中的服务器端口号不一致。

9.28 :更新和修复一些线程回调的空指针bug

  1. 获取ip 和第一次处理列表的时候,ip可能还未获取,就执行了处理列表,空指针异常[已解决]
  2. 写实现类的时候:
    1. firstRefresh 方法推送的数据,需要更新到 本地中(在上面9.24中第1条说,未初始化的问题,不知道怎么在这一个更新版本中,神奇的不出现了: 原因可能是有一个时间差:因为我debug的时候,查看自己自定义在构造中传入的参数还是空,过了一会就被赋值上了,表示父类已经初始化完成了,或许保险一点的解决方案是:在实现类中,firstRefresh 中需要先休眠几秒)
    2. 为什么要在firstRefresh 把获取的到数据更新到本地呢,是因为:在executor的时候才准备任务线程更新的参数,那么如果不是自己更新任务呢,那么本地就会有一段时间没有可用数据。
  3. 还要考虑到一个问题:如果第一次启动就是自己是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)入门与应用(转)

1 0
原创粉丝点击