声明:此博客为学习笔记,学习自极客学院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示例内容有(代码链接见本人末):