Distributed Priority Queue
分布式优先级队列
使用zk来实现一个分布式优先级队列。
注意,不建议使用ZK来做队列使用,原因可以参见 : 《Distributed Queue 的使用与分析》
1. 关键 API
org.apache.curator.framework.recipes.queue.QueueBuilder
org.apache.curator.framework.recipes.queue.QueueConsumer
org.apache.curator.framework.recipes.queue.QueueSerializer
org.apache.curator.framework.recipes.queue.DistributedPriorityQueue
2. 机制说明
其实ZK队列的最核心是 《Distributed Queue》。 优先级队列内部其实也是委托一个Distributed Queue来操作的,只是对Distributed Queue的一些扩展方法进行了定制。
3. 用法
3.1 创建
和Distributed Queue的方式一样,也主要是通过QueueBuilder来创建:
public static <T> QueueBuilder<T> builder(CuratorFramework client,
QueueConsumer<T> consumer,
QueueSerializer<T> serializer,
java.lang.String queuePath)
QueueBuilder<MessageType> builder = QueueBuilder.builder(client, consumer, serializer, path);
// 通过builder的方法进行一个参数设定
DistributedPriorityQueue<MessageType> queue = builder.buildPriorityQueue(minItemsBeforeRefresh);
与Distributed Queue的创建过程的差别就是,最后调用的是public DistributedPriorityQueue<T> buildPriorityQueue(int minItemsBeforeRefresh)
工厂方法。
优先级队列主要就是再队列元素的添加/移除时,会停止处理(投递)元素,而先进行一次队列同步刷新操作(对队列元素进行重排序)。
而参数minItemsBeforeRefresh
就是控制这个过程的。这个参数,指的是在可用队列,刷新队列之前,可以处理多少个元素。(数量越大,调度给消费者的消息吞吐越高(从本地cache中取),越小,则元素信息同步更频繁,同步开销越大,但实时性也更高)
由于,zk在节点变化时的通知机制问题,每一个元素被处理后,队列都会得到一个元素添加/移除的通知。这就会导致性能低下。通过设置minItemsBeforeRefresh
,来控制应用允许多少个消息处理可以不执行同步操作。
例如:如果队列要处理10个消息,它会去调用10次zk来检查状态(同步数据)。可以设置
minItemsBeforeRefresh
为10,那队列就会处理完10个消息之后,再去检查zk状态(同步数据)。
3.2 使用
老规矩,使用之前需要调用start()
,使用完成后,需要调用close()
。
消息放入队列,则需要指定优先级:
queue.put(aMessage, priority);
消息的消费,还是使用:QueueConsumer.consumeMessage()
3.3 安全消费
参见:《Distributed Queue 的使用与分析》的相关内容。
3.4 数据格式
参见:《Distributed Queue 的使用与分析》的相关内容。
4. 错误处理
参见:《Distributed Queue 的使用与分析》的相关内容。
5. 源码分析
5.1 类图
可以看出:
- DistributedPriorityQueue实现了QueueBase接口
- 内部委托给了DistributedQueue操作
5.2 类定义
public class DistributedPriorityQueue<T> implements Closeable, QueueBase<T>{}
- 实现了
java.io.Closeable
接口 - 实现了
org.apache.curator.framework.recipes.queue.QueueBase
接口
5.3 成员变量
public class DistributedPriorityQueue<T> implements Closeable, QueueBase<T>
{
private final DistributedQueue<T> queue;
}
- 只有一个
org.apache.curator.framework.recipes.queue.DistributedQueue
5.4 构造器
DistributedPriorityQueue
(
CuratorFramework client,
QueueConsumer<T> consumer,
QueueSerializer<T> serializer,
String queuePath,
ThreadFactory threadFactory,
Executor executor,
int minItemsBeforeRefresh,
String lockPath,
int maxItems,
boolean putInBackground,
int finalFlushMs
)
{
Preconditions.checkArgument(minItemsBeforeRefresh >= 0, "minItemsBeforeRefresh cannot be negative");
queue = new DistributedQueue<T>
(
client,
consumer,
serializer,
queuePath,
threadFactory,
executor,
minItemsBeforeRefresh,
true,
lockPath,
maxItems,
putInBackground,
finalFlushMs
);
}
- 除了进行
minItemsBeforeRefresh
的校验以外 - 就是对
DistributedQueue
的初始化- 注意使用
refreshOnWatch
了模式- 配合
minItemsBeforeRefresh
一起控制队列同步数据
- 配合
- 注意使用
5.5 启动
再来看看启动时的start()
方法:
public void start() throws Exception
{
queue.start();
}
- 很干脆,直接委托给了
org.apache.curator.framework.recipes.queue.DistributedQueue
参见:《Distributed Queue 的使用与分析》的相关内容。
5.6 关闭
@Override
public void close() throws IOException
{
queue.close();
}
- 同样,直接委托给了
org.apache.curator.framework.recipes.queue.DistributedQueue
参见:《Distributed Queue 的使用与分析》的相关内容。
5.7 产生消息
DistributedPriorityQueue在DistributedQueue的基础上增加了几个put方法。 其实基本上就是在DistributedQueue的put基础上,增加优先级参数。
public void put(T item, int priority) throws Exception
{
put(item, priority, 0, null);
}
public boolean put(T item, int priority, int maxWait, TimeUnit unit) throws Exception
{
queue.checkState();
String priorityHex = priorityToString(priority);
return queue.internalPut(item, null, queue.makeItemPath() + priorityHex, maxWait, unit);
}
这里最好,再对比着DistributedQueue的方法一起来看:
org.apache.curator.framework.recipes.queue.DistributedQueue#put(T, int, java.util.concurrent.TimeUnit)
public boolean put(T item, int maxWait, TimeUnit unit) throws Exception
{
checkState();
String path = makeItemPath();
return internalPut(item, null, path, maxWait, unit);
}
对比一下,看看DistributedPriorityQueue有哪些不一样的地方:
- 主要的差别就是对于元素节点的zk path的取值上
这里也就能发现,优先级是靠着元素节点名来进行排序。 在《Distributed Queue 的使用与分析》中,介绍过。拉取zk队列元素后,本地cache会对队列进行一次排序。(org.apache.curator.framework.recipes.queue.DistributedQueue#sortChildren
)
而这里的优先级队列,则是在添加元素时,对元素的名,进行控制。
5.7.1 优先级
按照org.apache.curator.framework.recipes.queue.DistributedQueue#makeItemPath
的逻辑,path最终会是path/queue-
的形式。 而DistributedPriorityQueue会在path/queue-
之后再加上一个表示优先级的字符串来生成最终的path。
这个优先级字符串是由org.apache.curator.framework.recipes.queue.DistributedPriorityQueue#priorityToString
方法实现的:
static String priorityToString(int priority)
{
long l = (long)priority & 0xFFFFFFFFL;
return String.format("%s%08X", (priority >= 0) ? "1" : "0", l);
}
- 这个方法是静态方法
分析一下逻辑:
- 取优先级int的后32位
- 为避免int的符号位的影响,操作时转成了long
- 将32位数值转成16进制大写字符
- 在字符串前加上一个“1/0”
- 1:表示正数
- 0:表示负数
- 这也避免了32位转16进制时,因符号位问题,导致负数的优先级会优于正数优先级的情况
所以,最终这个优先级字符串会是9个字符组成的。
6. 小结
通过以上的分析,DistributedPriorityQueue以一个委托的方式,具有了一个DistributedQueue的队列逻辑。
其最大区别,就在于元素节点的path命名上
- 通过在命名上带上优先级信息
- 利用DistributedQueue本地缓存调度时,会对元素节点进行string排序的特性,轻松的实现了一个分布式优先级队列