ZooKeeper实战之ZkClient客户端实现消息队列

声明:此博客为学习笔记,学习自极客学院ZooKeeper相关视频;本文内容是本人照着视频里的前辈所讲知识敲了
           一遍的记录,个别地方按照本人理解稍作修改。非常感谢众多大牛们的知识分享。

相关概念:

分布式队列(相关节点)架构图:

简述:当有新消息时,消息生产者会在queue节点下创建一个持久的有序节点(并存放相关数据);消息消费者负责读
           取queue节点的所有子节点来消费消息,并删除相应节点;具体细节见流程图。

消息生产者核心流程图:

消息消费者核心流程图:

 

软硬件环境:Windows10、IntelliJ IDEA、SpringBoot、ZkClient

准备工作:在pom.xml中引入相关依赖

<!-- https://mvnrepository.com/artifact/com.101tec/zkclient -->
<dependency>
    <groupId>com.101tec</groupId>
    <artifactId>zkclient</artifactId>
    <version>0.10</version>
</dependency>

 

ZooKeeper(消息)队列实现示例:

相关类总体说明:

DistributedQueueImpl:分布式队列的具体实现逻辑(含消息推送与拽取)。
       注:此示例仅为简单的示例。

MessageDataTO:消息信息封装类。

NodeDataIsNullException:自定义异常。

QueueTest:队列测试类。

各类细节:

DistributedQueueImpl:

import org.I0Itec.zkclient.IZkChildListener;
import org.I0Itec.zkclient.ZkClient;
import org.I0Itec.zkclient.exception.ZkNoNodeException;

import java.util.Comparator;
import java.util.List;
import java.util.concurrent.CountDownLatch;

/**
 * 分布式队列 的 实现
 *
 * @author JustryDeng
 * @date 2018/12/12 14:03
 */
public class DistributedQueueImpl<T> {

    /** ZkClient客户端 */
    private final ZkClient zkClient;

    /** queue节点路径 */
    private final String QUEUE_NODE_PATH;

    /** 节点分隔符 */
    private static final String NODE_SEPARATOR = "/";

    /** 子节点名称前缀 */
    private static final String CHILD_NAME_PREFIX = "msg_";

    /**
     * 构造器
     */
    public DistributedQueueImpl(ZkClient zkClient, String queueNodePath) {
        this.zkClient = zkClient;
        this.QUEUE_NODE_PATH = queueNodePath;
    }

    /**
     * 消息生产者 --- 推送消息
     *
     * @return 创建后的 该节点的真实路径
     * @date 2018/12/12 14:45
     */
    public String push(T data) {
        String childNodePath = QUEUE_NODE_PATH.concat(NODE_SEPARATOR).concat(CHILD_NAME_PREFIX);
        try {
            childNodePath = zkClient.createPersistentSequential(childNodePath, data);
        } catch (ZkNoNodeException e) { // 如果父路径不存在,则创建
            zkClient.createPersistent(QUEUE_NODE_PATH, true);
            push(data);
        }
        return childNodePath;
    }


    /**
     * 消息消费者 --- 消费消息
     * <p>
     * 提示: T必须可序列化,且创建ZkClient时,必须用SerializableSerializer序列化器
     *
     * @throws NodeDataIsNullException
     *         节点数据为null异常
     * @date 2018/12/12 14:45
     */
    public T pull() throws NodeDataIsNullException {
        List<String> list = zkClient.getChildren(QUEUE_NODE_PATH);
        if (list.size() == 0) {
            return null;
        }
        list.sort(Comparator.comparing(String::valueOf));
        String childNodePath;
        for (String childNodeName : list) {
            childNodePath = QUEUE_NODE_PATH.concat(NODE_SEPARATOR).concat(childNodeName);
            try {
                // 如果该节点没有数据,那么读取出来的即为null
                T childNodeData = zkClient.readData(childNodePath);
                zkClient.delete(childNodePath);
                if (childNodeData == null) {
                    throw new NodeDataIsNullException("节点" + childNodePath + "数据为null,已经删除该节点!");
                }
                return childNodeData;
            } catch (ZkNoNodeException e) {
                // 如果在执行delete操作时,发现该节点不存在(即:已经被别的消费者删除了)
                // 那么消费节点名次大的节点
            }
        }
        // QUEUE_NODE_PATH下没有任何子节点的话,返回null
        return null;
    }

    /**
     * 消息消费者 --- 消费消息
     *
     * @author JustryDeng
     * @date 2018/12/12 14:45
     */
    public T pullUntilGotMessage() throws Exception {
        while (true) {
            // 倒计时锁,长度设置为1
            final CountDownLatch latch = new CountDownLatch(1);
            // 监听器
            final IZkChildListener childListener = new IZkChildListener() {
                public void handleChildChange(String parentPath, List<String> currentChilds) {
                    System.out.println("父节点" + parentPath + "的子节点发生了变化!countDown!");
                    // 节点到子节点发生变化时,latch.countDown()
                    latch.countDown();
                }
            };
            // 订阅QUEUE_NODE_PATH节点的自及诶单改变事件
            zkClient.subscribeChildChanges(QUEUE_NODE_PATH, childListener);
            try {
                T node = pull();
                if (node != null) {
                    return node;
                } else {
                    // 说明QUEUE_NODE_PATH节点下此时没有任何子节点,那么继续等待,直到能消费到消息
                    latch.await();
                }
            } catch (NodeDataIsNullException e) { // 说明小得到了消息,不对该消息对应节点的数据本身就为null
                e.printStackTrace();
                return null;
            } finally {
                // 取消订阅
                zkClient.unsubscribeChildChanges(QUEUE_NODE_PATH, childListener);
            }
        }
    }

    /**
     * /queue子节点个数
     *
     * @author JustryDeng
     * @date 2018/12/12 14:06
     */
    public int size() {
        return zkClient.getChildren(QUEUE_NODE_PATH).size();
    }

    /**
     * /queue是否存在子节点
     *
     * @author JustryDeng
     * @date 2018/12/12 14:06
     */
    public boolean isEmpty() {
        return zkClient.getChildren(QUEUE_NODE_PATH).size() == 0;
    }

}

DistributedQueueImpl:

import java.io.Serializable;

/**
 * 数据封装类
 *
 * 注:由于创建zookeeper客户端ZkClient时,使用的序列化器是SerializableSerializer,
 *    所以此类需要可序列化,即:需要实现功能性接口Serializable
 *
 * @author JustryDeng
 * @date 2018/12/12 16:33
 */
public class MessageDataTO implements Serializable {

   private static final long serialVersionUID = -3251695575975145393L;

   /** 信息ID */
   private long id;

   /** 信息名字 */
   private String name;

   /**
    * 无参构造
    */
   public MessageDataTO() {
   }

   /**
    * 全参构造
    */
   public MessageDataTO(long id, String name) {
      this.id = id;
      this.name = name;
   }

   public long getId() {
      return id;
   }

   public void setId(long id) {
      this.id = id;
   }

   public String getName() {
      return name;
   }

   public void setName(String name) {
      this.name = name;
   }

   @Override
   public String toString() {
      return "MessageDataTO{id=" + id + ", name='" + name + "'}'";
   }
}

DistributedQueueImpl:

/**
 * 自定义 --- 节点数据为null异常
 *
 * @author JustryDeng
 * @date 2018/12/12 15:23
 */
public class NodeDataIsNullException extends Exception {

    public NodeDataIsNullException() {
    }

    public NodeDataIsNullException(String message) {
        super(message);
    }

    public NodeDataIsNullException(String message, Throwable cause) {
        super(message, cause);
    }

    public NodeDataIsNullException(Throwable cause) {
        super(cause);
    }

    public NodeDataIsNullException(String message, Throwable cause, boolean enableSuppression,
                                   boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}

DistributedQueueImpl:

import org.I0Itec.zkclient.ZkClient;
import org.I0Itec.zkclient.serialize.SerializableSerializer;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * 队列测试
 *
 * @author JustryDeng
 * @date 2018/12/12 13:28
 */
public class QueueTest {

    /** 程序入口 */
    public static void main(String[] args) throws InterruptedException {
        // 测试:如果没有消息,那么返回null
        // pushAndPullTest();
        //测试:如果没有消息,那么进行等待,直到消费到了消息
        pushAndPullUntilGotMessageTest();
    }


    /**
     * 测试 队列实现方式一:
     *    如果没有消息,那么返回null
     */
    private static void pushAndPullTest() {

        // zookeeper地址端口
        final String IP_PORT = "10.8.109.32:2181";

        // 会话超时时间
        final int SESSION_TIMEOUT = 5000;

        // 连接超时时间
        final int CONNECTION_TIMEOUT = 5000;

        // 会话超时时间
        final String QUEUE_PATH = "/queue";

        // zookeeper客户端
        ZkClient zkClient = new ZkClient(IP_PORT, SESSION_TIMEOUT, CONNECTION_TIMEOUT, new SerializableSerializer());

        // 分布式队列示例
        DistributedQueueImpl<MessageDataTO> queueImpl = new DistributedQueueImpl<>(zkClient, QUEUE_PATH);

        // 数据
        final MessageDataTO messageDeng = new MessageDataTO(1L, "邓消息");
        final MessageDataTO messageLi = new MessageDataTO(2L, "李消息");
        try {
            // 消息生产者 生产(推送)两个消息
            String pathDeng = queueImpl.push(messageDeng);
            System.out.println("消息messageDeng对应的节点路径为" + pathDeng);
            String pathLi = queueImpl.push(messageLi);
            System.out.println("消息messageLi对应的节点路径为" + pathLi);

            Thread.sleep(1000);

            // 消息消费者 消费两个消息
            MessageDataTO gotMessageOne;
            try {
                gotMessageOne = queueImpl.pull();
            } catch (NodeDataIsNullException e) {
                // 说明消费到了消息,不过该消息本身九尾null
                gotMessageOne = new MessageDataTO();
            }
            MessageDataTO gotMessageTwo;
            try {
                gotMessageTwo = queueImpl.pull();
            } catch (NodeDataIsNullException e) {
                // 说明消费到了消息,不过该消息本身九尾null
                gotMessageTwo = new MessageDataTO();
            }

            System.out.println("理论上,得到的第一个消息应该是:" + messageDeng + "\t实际上,得到的第一个消息是:"
                               + gotMessageOne);
            System.out.println("理论上,得到的第二个消息应该是:" + messageLi + "\t实际上,得到的第一个消息是:"
                    + gotMessageTwo);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 测试 队列实现方式二:
     *    如果没有消息,那么进行等待,直到消费到了消息
     */
    private static void pushAndPullUntilGotMessageTest() throws InterruptedException {

        // zookeeper地址端口
        final String IP_PORT = "10.8.109.32:2181";

        // 会话超时时间
        final int SESSION_TIMEOUT = 5000;

        // 连接超时时间
        final int CONNECTION_TIMEOUT = 5000;

        // 会话超时时间
        final String QUEUE_PATH = "/queue";

        // zookeeper客户端
        ZkClient zkClient = new ZkClient(IP_PORT, SESSION_TIMEOUT, CONNECTION_TIMEOUT, new SerializableSerializer());

        // 分布式队列示例
        DistributedQueueImpl<MessageDataTO> queueImpl = new DistributedQueueImpl<>(zkClient, QUEUE_PATH);


        // 创建 调度线程池
        ScheduledExecutorService delayExector = Executors.newScheduledThreadPool(1);
        // 延迟时间
        final int delayTime = 5;

        // 数据
        final MessageDataTO messageDeng = new MessageDataTO(1L, "邓消息");
        final MessageDataTO messageLi = new MessageDataTO(2L, "李消息");
        try {

            delayExector.schedule(() -> {
                    try {
                        System.out.println("---> 开始推送消息!");
                        // 消息生产者 生产(推送)两个消息
                        String pathDeng = queueImpl.push(messageDeng);
                        System.out.println("消息messageDeng对应的节点路径为" + pathDeng);
                        String pathLi = queueImpl.push(messageLi);
                        System.out.println("消息messageLi对应的节点路径为" + pathLi);
                        System.out.println("---> 推送消息完成!");
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
            }, delayTime , TimeUnit.SECONDS);
            System.out.println("---> 开始试着拽取消息。。。");
            // 消息消费者 消费两个消息
            MessageDataTO gotMessageOne = queueImpl.pullUntilGotMessage();
            System.out.println("拽取(消费)到了消息:" + gotMessageOne);
            MessageDataTO gotMessageTwo = queueImpl.pullUntilGotMessage();
            System.out.println("拽取(消费)到了消息:" + gotMessageTwo);
            System.out.println("---> 拽取消息完成!");
        } catch (Exception e) {
            e.printStackTrace();
        } finally{
            // 关闭线程池,释放资源
            delayExector.shutdown();
            // 阻塞当前线程一段时间
            // 如果在这段时间结束之后,所有tasks还没有执行完毕,那么当前线程不再阻塞,往下执行
            // 如果在这段时间结束之前,所有tasks都执行完毕,那么不论这段时间还剩多久,都不再阻塞,往下执行
            delayExector.awaitTermination(10, TimeUnit.SECONDS);
        }
    }

}

 

测试一下:

前提条件:启动对应的ZooKeeper服务器,开放端口(或关闭防火墙)。

使QueueTest类的main方法执行 pushAndPullTest()

 

控制台输出:

使QueueTest类的main方法执行 pushAndPullUntilGotMessageTest()

控制台输出:

由此可见:实现简单的分布式队列成功!

所有zookeeper示例内容有(代码链接见本人末):

微笑如有不当之处,欢迎指正

微笑zookeeper示例代码托管链接
              
https://github.com/JustryDeng/CommonRepository
微笑参考书籍
             《ZooKeeper-分布式过程协同技术详解》
                        Flavio Junqueira Benjamin Reed 著, 谢超 周贵卿 译
微笑学习视频(推荐观看学习)
             极客学院ZooKeeper相关视频
微笑本文已经被收录进《程序员成长笔记(三)》,笔者JustryDeng

猜你喜欢

转载自blog.csdn.net/justry_deng/article/details/84977228