由于zookeeper 的分布式一致性的特点 zk也可以用来实现分布式的队列:一般适合于数据量比较小对一致性要求比较高的,如果数据量大的推荐使用kafka,因为zk 本身不适合高并发的读写。
具体结构什么样子的呢 如下: 网上找了一个图
使用路径/queue下的znode节点表示队列中的元素,图上的节点都是持久化的顺序节点,这些znode 名字的后缀数字代表位置 位置越小越靠前。
那么具体怎么实现呢 其实在源码中 org.apache.zookeeper.recipes.queue.DistributedQueue
。
已经实现了。下面来做一个源码解析
public DistributedQueue(ZooKeeper zookeeper, String dir, List<ACL> acl) {
this.dir = dir;
if (acl != null) {
this.acl = acl;
}
this.zookeeper = zookeeper;
}
首先实例化对象传入 zookeeper 实例 和相应的目录(dir) 以及相应的权限
如果没有传入权限 会默认使用 OPEN_ACL_UNSAFE 这个权限比较高 基本包含所有
private List<ACL> acl = ZooDefs.Ids.OPEN_ACL_UNSAFE;
public boolean offer(byte[] data) throws KeeperException, InterruptedException {
for (; ; ) {
try {
zookeeper.create(dir + "/" + prefix, data, acl, CreateMode.PERSISTENT_SEQUENTIAL);
return true;
} catch (KeeperException.NoNodeException e) {
zookeeper.create(dir, new byte[0], acl, CreateMode.PERSISTENT);
}
}
}
PERSISTENT_SEQUENTIAL 顺序自动编号持久化节点,这种节点会根据当前已存在的节点数自动加 1。 在这里我们我们通过通过zk 的能创建自动编号持久化节点,进行入队列操作。
下面看下出队列的操作 一般都有两种一个是删除数据 另外一种是不删除数据。下面我们先看一下出不删除数据那种
public byte[] peek() throws KeeperException, InterruptedException {
try {
return element();
} catch (NoSuchElementException e) {
return null;
}
}
public byte[] element() throws NoSuchElementException, KeeperException, InterruptedException {
Map<Long, String> orderedChildren;
// element, take, and remove follow the same pattern.
// We want to return the child node with the smallest sequence number.
// Since other clients are remove()ing and take()ing nodes concurrently,
// the child with the smallest sequence number in orderedChildren might be gone by the time we check.
// We don't call getChildren again until we have tried the rest of the nodes in sequence order.
while (true) {
try {
// 返回一个已经排序的子列表的集合
orderedChildren = orderedChildren(null);
} catch (KeeperException.NoNodeException e) {
throw new NoSuchElementException();
}
if (orderedChildren.size() == 0) {
throw new NoSuchElementException();
}
// 遍历返回第一个对象的值
// 这里为什么要用循环呢 应该 可能存在在获取的同时被其他的节点删除了
// 所以用这样的方式 如果被删除了 则获取下一个
for (String headNode : orderedChildren.values()) {
if (headNode != null) {
try {
return zookeeper.getData(dir + "/" + headNode, false, null);
} catch (KeeperException.NoNodeException e) {
//Another client removed the node first, try next
}
}
}
}
}
下面看一下删除数据的出队方式其实和上面差不多 就是多了一步删除操作。
public byte[] poll() throws KeeperException, InterruptedException {
try {
return remove();
} catch (NoSuchElementException e) {
return null;
}
}
public byte[] remove() throws NoSuchElementException, KeeperException, InterruptedException {
Map<Long, String> orderedChildren;
// Same as for element. Should refactor this.
while (true) {
try {
orderedChildren = orderedChildren(null);
} catch (KeeperException.NoNodeException e) {
throw new NoSuchElementException();
}
if (orderedChildren.size() == 0) {
throw new NoSuchElementException();
}
for (String headNode : orderedChildren.values()) {
String path = dir + "/" + headNode;
try {
byte[] data = zookeeper.getData(path, false, null);
zookeeper.delete(path, -1);
return data;
} catch (KeeperException.NoNodeException e) {
// Another client deleted the node first.
}
}
}
}
我们发现很多方法都调用了 orderedChildren 我们来看下都干了什么,首先构造一个 TreeMap 通过treemap 的性质 来保证顺序。
private Map<Long, String> orderedChildren(Watcher watcher) throws KeeperException, InterruptedException {
Map<Long, String> orderedChildren = new TreeMap<>();
List<String> childNames;
childNames = zookeeper.getChildren(dir, watcher);
for (String childName : childNames) {
try {
//Check format regionMatches() 方法用于检测两个字符串在一个区域内是否相等。
if (!childName.regionMatches(0, prefix, 0, prefix.length())) {
LOG.warn("Found child node with improper name: {}", childName);
continue;
}
String suffix = childName.substring(prefix.length());
Long childId = Long.parseLong(suffix);
orderedChildren.put(childId, childName);
} catch (NumberFormatException e) {
LOG.warn("Found child node with improper format : {}", childName, e);
}
}
return orderedChildren;
}