Distributed Queue
分布式队列
一个基于ZK的分布式队列实现。
放入的消息可以保证顺序(基于zk的有序持久化节点)。
对于单个消费者来说,队列是一个FIFO(先进先出)的方式。 如果需要控制顺序,可以为消费者指定一个LeaderSelector
,来定制消费策略。
1. 注意点
其实,实际应用中很少会用zk做队列使用。
Curator自身也不建议使用zk当作队列使用
IMPORTANT - We recommend that you do NOT use ZooKeeper for Queues. Please see Tech Note 4 for details.
Tech Note 4 ZooKeeper makes a very bad Queue source.
The ZooKeeper recipes page lists Queues as a possible use-case for ZooKeeper. Curator includes several Queue recipes. In our experience, however, it is a bad idea to use ZooKeeper as a Queue:
- ZooKeeper has a 1MB transport limitation. In practice this means that ZNodes must be relatively small. Typically, queues can contain many thousands of messages.
- ZooKeeper can slow down considerably on startup if there are many large ZNodes. This will be common if you are using ZooKeeper for queues. You will need to significantly increase initLimit and syncLimit.
- If a ZNode gets too big it can be extremely difficult to clean. getChildren() will fail on the node. At Netflix we had to create a special-purpose program that had a huge value for jute.maxbuffer in order to get the nodes and delete them.
- ZooKeeper can start to perform badly if there are many nodes with thousands of children.
- The ZooKeeper database is kept entirely in memory. So, you can never have more messages than can fit in memory.
虽然Curator提供了几种zk的队列方案,但仍然不建议使用zk作为队列来使用,因为:
- zk对于传输数据有一个 1MB 的大小限制。
- 这就意味着实际中zk节点ZNodes必须设计的很小
- 但实际中队列通常都存放着数以千计的消息
- 如果有很多大的ZNodes,那会严重拖慢的zk启动过程。
- 包括zk节点之间的同步过程
- 如果正要用zk当队列,最好去调整
initLimit
与syncLimit
- 如果一个ZNode过大,也会导致清理变得困难
- 也会导致
getChildren()
方法失败- Netflix不得不设计一个特殊的机制来处理这个大体积的nodes
- 如果zk中某个node下有数千子节点,也会严重拖累zk性能
- zk中的数据都会放置在内存中。
虽然,zk天生不适合做队列,但是还是来看看Curator的实现,学习一下Curator的设计
2. 关键 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.DistributedIdQueue
3. 用法
3.1 创建
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);
... more builder method calls as needed ...
DistributedQueue<MessageType queue = builder.build();
- 通过一个Builder模式来创建
3.2 使用
队列开始使用之前需要调用start()
方法。当用完之后需要调用close()
。
生产消息
queue.put(aMessage);
这样消息就会到达消费端的QueueConsumer.consumeMessage()
方法。
3.3 安全消费
一般情况下,消息投递之后就被移除,不会等待消费端调用完成。
所以,Curator还提供一种更加原子化的方式:在消费端成功返回后再移除消息。要开启这个模式,可以调用Builder的lockPath()
。
- 通过锁的方式来保障消息是可回收的
- 用锁来保障已投递的消息不被其他消费端获取
- 等待投递的消费端完成返回
- 处理失败或者处理过程异常中断,消息会被再次投递
- 用锁的方式来处理,难免带来更多的性能开销
3.4 数据格式
分布式队列写入的消息会使用以下格式:
偏移量 | SIZE | 描述 |
---|---|---|
0 | 4 | 格式版本,当前版本:0x00010001 |
4 | 1 | 指令:0x01 = 消息, 0x02 = 数据结束 |
5 | 4 | 消息字节长度 |
9 | n | 消息序列化后的字节 |
9 n | ... | 下一组(指令-长度-序列化字节),直到数据结束 |
4. 错误处理
QueueConsumer
类继承了ConnectionStateListener
。当队列启动完成,会自动添加监听器。 不论如何,使用DistributedQueue
必须要注意zk连接状态的变化。
如果中断(SUSPENDED
)状态发生,实例必须认为队列没有更新,直到重连成功(SUSPENDED
)。 如果连接丢失(LOST
),则应该认定队列彻底不可用。
5. 源码分析
5.1 类图
先看看Curator Queue的几个核心对象的关系:
可以看出:
- 所有的队列都实现
org.apache.curator.framework.recipes.queue.QueueBase
接口 - 最核心的队列实现是:
org.apache.curator.framework.recipes.queue.DistributedQueue
5.2 类定义
public class DistributedQueue<T> implements QueueBase<T> {...}
- 实现了
org.apache.curator.framework.recipes.queue.QueueBase
接口
public interface QueueBase<T> extends Closeable{
void start() throws Exception;
ListenerContainer<QueuePutListener<T>> getPutListenerContainer();
void setErrorMode(ErrorMode newErrorMode);
boolean flushPuts(long waitTime, TimeUnit timeUnit) throws InterruptedException;
int getLastMessageCount();
}
- 继承与
java.io.Closeable
接口 - 定义了一些队列的通用方法
public class DistributedQueue<T> implements QueueBase<T> {}
- DistributedQueue 实现了
org.apache.curator.framework.recipes.queue.QueueBase
接口
5.3 成员变量
public class DistributedQueue<T> implements QueueBase<T>
{
private final Logger log = LoggerFactory.getLogger(getClass());
private final CuratorFramework client;
private final QueueSerializer<T> serializer;
private final String queuePath;
private final Executor executor;
private final ExecutorService service;
private final AtomicReference<State> state = new AtomicReference<State>(State.LATENT);
private final QueueConsumer<T> consumer;
private final int minItemsBeforeRefresh;
private final boolean refreshOnWatch;
private final boolean isProducerOnly;
private final String lockPath;
private final AtomicReference<ErrorMode> errorMode = new AtomicReference<ErrorMode>(ErrorMode.REQUEUE);
private final ListenerContainer<QueuePutListener<T>> putListenerContainer = new ListenerContainer<QueuePutListener<T>>();
private final AtomicInteger lastChildCount = new AtomicInteger(0);
private final int maxItems;
private final int finalFlushMs;
private final boolean putInBackground;
private final ChildrenCache childrenCache;
private final AtomicInteger putCount = new AtomicInteger(0);
private enum State
{
LATENT,
STARTED,
STOPPED
}
@VisibleForTesting
protected enum ProcessType
{
NORMAL,
REMOVE
}
private static final String QUEUE_ITEM_NAME = "queue-";
- log
- client
- serializer
org.apache.curator.framework.recipes.queue.QueueSerializer
- 对队列元素进行序列化/反序列的操作
- queuePath
- 队列对应的zk路径
- executor
java.util.concurrent.Executor
- 线程池
- 处理消费消费任务的线程池
- service
java.util.concurrent.ExecutorService
- 线程池
- 用以处理从队列中拉取消息,以及内部异步任务
- state
org.apache.curator.framework.recipes.queue.DistributedQueue.State
- 内部枚举
- 状态
AtomicReference
- consumer
org.apache.curator.framework.recipes.queue.QueueConsumer
- 队列消费者
- minItemsBeforeRefresh
- 控制队列调度消息的最小数量
- refreshOnWatch
- 拉取消息后是否异步调度消费
- isProducerOnly
- 当不指定消费者时,队列工作于仅生产模式
- 此模式,不会拉取消息
- lockPath
- 占位锁对应的zk路径
- errorMode
org.apache.curator.framework.recipes.queue.ErrorMode
- 消费消息时不同的错误处理方式
- 枚举
- REQUEUE 重新放入队列
- DELETE 删除
AtomicReference
- putListenerContainer
org.apache.curator.framework.listen.ListenerContainer
- 队列放入消息的监听器容器
- lastChildCount
- 子节点数量
AtomicInteger
- maxItems
- 队列中最大消息数量
- finalFlushMs
- 关闭时,延迟等待时间
- 可以让关闭时,等待还未投递的消息完成投递动作
- putInBackground
- 是否异步发送
- 利用curator回调
- childrenCache
org.apache.curator.framework.recipes.queue.ChildrenCache
- 子节点缓存
- 消息数据的缓存
- putCount
- 入队消息的计数器
AtomicInteger
5.4 构造器
DistributedQueue
(
CuratorFramework client,
QueueConsumer<T> consumer,
QueueSerializer<T> serializer,
String queuePath,
ThreadFactory threadFactory,
Executor executor,
int minItemsBeforeRefresh,
boolean refreshOnWatch,
String lockPath,
int maxItems,
boolean putInBackground,
int finalFlushMs
)
{
Preconditions.checkNotNull(client, "client cannot be null");
Preconditions.checkNotNull(serializer, "serializer cannot be null");
Preconditions.checkNotNull(threadFactory, "threadFactory cannot be null");
Preconditions.checkNotNull(executor, "executor cannot be null");
Preconditions.checkArgument(maxItems > 0, "maxItems must be a positive number");
isProducerOnly = (consumer == null);
this.lockPath = (lockPath == null) ? null : PathUtils.validatePath(lockPath);
this.putInBackground = putInBackground;
this.consumer = consumer;
this.minItemsBeforeRefresh = minItemsBeforeRefresh;
this.refreshOnWatch = refreshOnWatch;
this.client = client;
this.serializer = serializer;
this.queuePath = PathUtils.validatePath(queuePath);
this.executor = executor;
this.maxItems = maxItems;
this.finalFlushMs = finalFlushMs;
service = Executors.newFixedThreadPool(2, threadFactory);
childrenCache = new ChildrenCache(client, queuePath);
if ( (maxItems != QueueBuilder.NOT_SET) && putInBackground )
{
log.warn("Bounded queues should set putInBackground(false) in the builder. Putting in the background will result in spotty maxItem consistency.");
}
}
}
- 访问权限为package
- 构造器的参数比较多
- 所以提供一个Builder模式:
org.apache.curator.framework.recipes.queue.QueueBuilder
- 所以提供一个Builder模式:
可以看到,基本都是简单赋值,但是有几个属性经过了一些处理:
- service
- 默认使用了
newFixedThreadPool
,大小为2的线程池- 任务队列
java.util.concurrent.LinkedBlockingQueue
- FIFO
- 理论上存在内存溢出问题
- 任务队列
- 默认使用了
- childrenCache
- 对
queuePath
进行了缓存
- 对
还有一个地方需要注意:
if ( (maxItems != QueueBuilder.NOT_SET) && putInBackground )
{
log.warn("Bounded queues should set putInBackground(false) in the builder. Putting in the background will result in spotty maxItem consistency.");
}
当设置了maxItems
,指定队列最大存放的消息数量;并且使用异步(回调)发送的方式时。 Curator会有一条警告日志。
Bounded queues should set putInBackground(false) in the builder. Putting in the background will result in spotty maxItem consistency.
有界队列应该使用同步发送的方式来构建。如果使用异步发送的方式会导致
maxItems
数量不一致。
5.5 启动
在使用之前需要调用start()
方法:
public void start() throws Exception
{
if ( !state.compareAndSet(State.LATENT, State.STARTED) )
{
throw new IllegalStateException();
}
try
{
client.create().creatingParentContainersIfNeeded().forPath(queuePath);
}
catch ( KeeperException.NodeExistsException ignore )
{
// this is OK
}
if ( lockPath != null )
{
try
{
client.create().creatingParentContainersIfNeeded().forPath(lockPath);
}
catch ( KeeperException.NodeExistsException ignore )
{
// this is OK
}
}
if ( !isProducerOnly || (maxItems != QueueBuilder.NOT_SET) )
{
childrenCache.start();
}
if ( !isProducerOnly )
{
service.submit
(
new Callable<Object>()
{
@Override
public Object call()
{
runLoop();
return null;
}
}
);
}
}
- 先使用CAS操作更新状态
- 创建队列的zk节点
- 如果需要,创建对应分布式锁的zk节点
- 如果仅仅是生产者角色,并且设定为有界队列
- 启动子节点缓存
- 如果不仅是生产者
- 则触发一个异步操作调用
runLoop()
方法
- 则触发一个异步操作调用
启动过程的逻辑并不复杂,但是有两个方法需要进一步分析:
5.5.1 启动子节点缓存
调用org.apache.curator.framework.recipes.queue.ChildrenCache#start
方法:
void start() throws Exception
{
sync(true);
}
private synchronized void sync(boolean watched) throws Exception
{
if ( watched )
{
client.getChildren().usingWatcher(watcher).inBackground(callback).forPath(path);
}
else
{
client.getChildren().inBackground(callback).forPath(path);
}
}
private final CuratorWatcher watcher = new CuratorWatcher()
{
@Override
public void process(WatchedEvent event) throws Exception
{
if ( !isClosed.get() )
{
sync(true);
}
}
};
private final BackgroundCallback callback = new BackgroundCallback()
{
@Override
public void processResult(CuratorFramework client, CuratorEvent event) throws Exception
{
if ( event.getResultCode() == KeeperException.Code.OK.intValue() )
{
setNewChildren(event.getChildren());
}
}
};
private synchronized void setNewChildren(List<String> newChildren)
{
if ( newChildren != null )
{
Data currentData = children.get();
children.set(new Data(newChildren, currentData.version + 1));
notifyFromCallback();
}
}
- 使用观察者模式(监听器),再必要时恢复子节点信息
- 使用回调处理队列的子节点信息
- 使用一个版本号对子节点列表进行本地缓存
- 使用不可变List对子节点列表进行包装
- 使用版本号跟踪,避免ABA问题
- 使用
AtomicReference
进行原子化包装 - 子节点信息使用一个内部类进行封装
org.apache.curator.framework.recipes.queue.ChildrenCache.Data
5.5.2 异步操作runLoop()
方法
在队列启动过程中,如果不仅仅是生产者模式,还会执行一个额外的异步调用,来执行org.apache.curator.framework.recipes.queue.DistributedQueue#runLoop
方法:
private void runLoop()
{
long currentVersion = -1;
long maxWaitMs = -1;
try
{
while ( state.get() == State.STARTED )
{
try
{
ChildrenCache.Data data = (maxWaitMs > 0) ? childrenCache.blockingNextGetData(currentVersion, maxWaitMs, TimeUnit.MILLISECONDS) : childrenCache.blockingNextGetData(currentVersion);
currentVersion = data.version;
List<String> children = Lists.newArrayList(data.children);
sortChildren(children); // makes sure items are processed in the correct order
if ( children.size() > 0 )
{
maxWaitMs = getDelay(children.get(0));
if ( maxWaitMs > 0 )
{
continue;
}
}
else
{
continue;
}
processChildren(children, currentVersion);
}
catch ( InterruptedException e )
{
// swallow the interrupt as it's only possible from either a background
// operation and, thus, doesn't apply to this loop or the instance
// is being closed in which case the while test will get it
}
}
}
catch ( Exception e )
{
log.error("Exception caught in background handler", e);
}
}
//----------------------------------------
//org.apache.curator.framework.recipes.queue.ChildrenCache
//----------------------------------------
Data blockingNextGetData(long startVersion) throws InterruptedException
{
return blockingNextGetData(startVersion, 0, null);
}
synchronized Data blockingNextGetData(long startVersion, long maxWait, TimeUnit unit) throws InterruptedException
{
long startMs = System.currentTimeMillis();
boolean hasMaxWait = (unit != null);
long maxWaitMs = hasMaxWait ? unit.toMillis(maxWait) : -1;
while ( startVersion == children.get().version )
{
if ( hasMaxWait )
{
long elapsedMs = System.currentTimeMillis() - startMs;
long thisWaitMs = maxWaitMs - elapsedMs;
if ( thisWaitMs <= 0 )
{
break;
}
wait(thisWaitMs);
}
else
{
wait();
}
}
return children.get();
}
队列启动后,不停的尝试拉取消息:
- 获取子节点缓存
- 如果版本相同,说明本地缓存比较旧需要等待同步后才能返回新数据
- 启动时获取版本
-1
- 而
ChildrenCache.Data
的默认版本是0
- 所以启动时,会直接返回初始化的
ChildrenCache.Data
- 启动时获取版本
- 如果版本相同,说明本地缓存比较旧需要等待同步后才能返回新数据
- 对子节点进行字典排序
- 如果有子节点信息
- 使用
getDelay
根据当前子节点信息获取下一次获取新消息的延迟时间- 子类可以覆盖这个方法,进行队列的扩展
- 使用
- 调用
processChildren(children, currentVersion);
处理子节点数据
5.5.3 子节点数据处理
org.apache.curator.framework.recipes.queue.DistributedQueue#processChildren
方法:
private void processChildren(List<String> children, long currentVersion) throws Exception
{
final Semaphore processedLatch = new Semaphore(0);
final boolean isUsingLockSafety = (lockPath != null);
int min = minItemsBeforeRefresh;
for ( final String itemNode : children )
{
if ( Thread.currentThread().isInterrupted() )
{
processedLatch.release(children.size());
break;
}
if ( !itemNode.startsWith(QUEUE_ITEM_NAME) )
{
log.warn("Foreign node in queue path: " + itemNode);
processedLatch.release();
continue;
}
if ( min-- <= 0 )
{
if ( refreshOnWatch && (currentVersion != childrenCache.getData().version) )
{
processedLatch.release(children.size());
break;
}
}
if ( getDelay(itemNode) > 0 )
{
processedLatch.release();
continue;
}
executor.execute
(
new Runnable()
{
@Override
public void run()
{
try
{
if ( isUsingLockSafety )
{
processWithLockSafety(itemNode, ProcessType.NORMAL);
}
else
{
processNormally(itemNode, ProcessType.NORMAL);
}
}
catch ( Exception e )
{
ThreadUtils.checkInterrupted(e);
log.error("Error processing message at " + itemNode, e);
}
finally
{
processedLatch.release();
}
}
}
);
}
processedLatch.acquire(children.size());
}
- 使用一个数量为0的信号量
java.util.concurrent.Semaphore
- 当作闭锁使用
为每一个子节点调度一个异步任务:
- 如果指定了
lockPath
,则使用加锁的方式,安全的处理消息节点 - 否则,以普通的方式处理消息节点
- 处理完成后,释放信号量
最后,利用信号量阻塞,等待当前版本的子节点全部处理完成
5.5.3.1 加锁处理消息节点
org.apache.curator.framework.recipes.queue.DistributedQueue#processWithLockSafety
方法:
protected boolean processWithLockSafety(String itemNode, ProcessType type) throws Exception
{
String lockNodePath = ZKPaths.makePath(lockPath, itemNode);
boolean lockCreated = false;
try
{
client.create().withMode(CreateMode.EPHEMERAL).forPath(lockNodePath);
lockCreated = true;
String itemPath = ZKPaths.makePath(queuePath, itemNode);
boolean requeue = false;
byte[] bytes = null;
if ( type == ProcessType.NORMAL )
{
bytes = client.getData().forPath(itemPath);
requeue = (processMessageBytes(itemNode, bytes) == ProcessMessageBytesCode.REQUEUE);
}
if ( requeue )
{
client.inTransaction()
.delete().forPath(itemPath)
.and()
.create().withMode(CreateMode.PERSISTENT_SEQUENTIAL).forPath(makeRequeueItemPath(itemPath), bytes)
.and()
.commit();
}
else
{
client.delete().forPath(itemPath);
}
return true;
}
catch ( KeeperException.NodeExistsException ignore )
{
// another process got it
}
catch ( KeeperException.NoNodeException ignore )
{
// another process got it
}
catch ( KeeperException.BadVersionException ignore )
{
// another process got it
}
finally
{
if ( lockCreated )
{
client.delete().guaranteed().forPath(lockNodePath);
}
}
return false;
}
- 这里并没有使用
org.apache.curator.framework.recipes.locks.InterProcessMutex
分布式重入锁- 而仅仅使用一个ZK临时节点的方式,进行占位
- 以此判断一个消息是否正在被某个消费者处理
- 如果处理的结果是需要重入队列,则原子化(Curator的zk事务)执行:
- 删除原队列中的消息节点
- 将消息数据重新写入另一个队列
- 具体是哪个队列,由
makeRequeueItemPath
方法决定- 子类覆盖这个方法,可以定制不同的重入策略
- 具体是哪个队列,由
- 如果处理结果不需要重入,则删除原队列中的消息节点
- 安全的(
guaranteed
)清理掉占位的临时节点
5.5.3.2 普通方式处理消息节点
org.apache.curator.framework.recipes.queue.DistributedQueue#processNormally
方法:
private boolean processNormally(String itemNode, ProcessType type) throws Exception
{
try
{
String itemPath = ZKPaths.makePath(queuePath, itemNode);
Stat stat = new Stat();
byte[] bytes = null;
if ( type == ProcessType.NORMAL )
{
bytes = client.getData().storingStatIn(stat).forPath(itemPath);
}
if ( client.getState() == CuratorFrameworkState.STARTED )
{
client.delete().withVersion(stat.getVersion()).forPath(itemPath);
}
if ( type == ProcessType.NORMAL )
{
processMessageBytes(itemNode, bytes);
}
return true;
}
catch ( KeeperException.NodeExistsException ignore )
{
// another process got it
}
catch ( KeeperException.NoNodeException ignore )
{
// another process got it
}
catch ( KeeperException.BadVersionException ignore )
{
// another process got it
}
return false;
}
- 获取节点数据后,直接删除了节点
- 然后本地调用消费者处理消息
5.5.4 小结
重新整理一下启动过程:
- 更新启动状态
- 创建队列路径
- 如果需要,创建队列消息消费时锁的路径
- 如果指定了消费者,或者是一个有界队列,则启动子节点缓存(childrenCache)
- 添加监听器
- 用以在各种意外情况下,自动同步缓存
- 拉取消息节点
notifyAll()
通知等待的消费者
- 添加监听器
- 如果指定了消费者,则异步启动轮询任务拉取消息节点
- 阻塞获取队列消息节点
- 按
getDelay
方法,进行延迟(定制消息的消费策略) - 处理消息
- 对消息的消费进行一定的过滤和调度
- 最小刷新率
- 延迟
- 为每一个可以处理的消息节点创建一个异步处理任务
- 拉取消息节点数据
- 如需安全消费,则使用临时节点进行占位
- 调用
consumer
消费消息 - 按照处理结果进行消息收尾工作
- 重入队列
- 可以通过
makeRequeueItemPath
方法,定制重入策略
- 可以通过
- 删除
- 重入队列
- 拉取消息节点数据
- 对消息的消费进行一定的过滤和调度
5.6 关闭
当队列使用完之后,需要调用close()
方法:
public void close() throws IOException
{
if ( state.compareAndSet(State.STARTED, State.STOPPED) )
{
if ( finalFlushMs > 0 )
{
try
{
flushPuts(finalFlushMs, TimeUnit.MILLISECONDS);
}
catch ( InterruptedException e )
{
Thread.currentThread().interrupt();
}
}
CloseableUtils.closeQuietly(childrenCache);
putListenerContainer.clear();
service.shutdownNow();
}
}
当CAS安全更新STOPPED
状态后:
- 如果配置了延迟毫秒数
- 则调用
flushPuts
方法,等待消息处理的结果 - 相当于优雅关机
- 则调用
5.6.1 flushPuts方法
阻塞等待消息全部投递到zk
public boolean flushPuts(long waitTime, TimeUnit timeUnit) throws InterruptedException
{
long msWaitRemaining = TimeUnit.MILLISECONDS.convert(waitTime, timeUnit);
synchronized(putCount)
{
while ( putCount.get() > 0 )
{
if ( msWaitRemaining <= 0 )
{
return false;
}
long startMs = System.currentTimeMillis();
putCount.wait(msWaitRemaining);
long elapsedMs = System.currentTimeMillis() - startMs;
msWaitRemaining -= elapsedMs;
}
}
return true;
}
- 如果投递计数器不为0,则等待。说明还有消息正在处理。
- 关闭时最多等待参数
finalFlushMs
,单位:毫秒
5.7 产生消息
生产者产生消息后,可以调用put
方法,将消息放入队列:
public void put(T item) throws Exception
{
put(item, 0, null);
}
public boolean put(T item, int maxWait, TimeUnit unit) throws Exception
{
checkState();
String path = makeItemPath();
return internalPut(item, null, path, maxWait, unit);
}
- 只要队列元素的数量没有超过
maxItems
,则无需等待,直接返回 - 否则,等待队列消息消费后,才能放入
- 可以通过参数,指定等待的时间
实际的入队列操作是由org.apache.curator.framework.recipes.queue.DistributedQueue#internalPut
方法实现的:
boolean internalPut(final T item, MultiItem<T> multiItem, String path, int maxWait, TimeUnit unit) throws Exception
{
if ( !blockIfMaxed(maxWait, unit) )
{
return false;
}
final MultiItem<T> givenMultiItem = multiItem;
if ( item != null )
{
final AtomicReference<T> ref = new AtomicReference<T>(item);
multiItem = new MultiItem<T>()
{
@Override
public T nextItem() throws Exception
{
return ref.getAndSet(null);
}
};
}
putCount.incrementAndGet();
byte[] bytes = ItemSerializer.serialize(multiItem, serializer);
if ( putInBackground )
{
doPutInBackground(item, path, givenMultiItem, bytes);
}
else
{
doPutInForeground(item, path, givenMultiItem, bytes);
}
return true;
}
- 如果指定了等待时间,则进行判定是否需要阻塞等待
- 如果已经超过最大数量,则会触发一次同步(再次拉取zk中的消息节点)
- 如果,版本(消息数据跟踪版本)没有变化,则判定消息进入队列失败
- 将消息item包装成
org.apache.curator.framework.recipes.queue.MultiItem
- 更新入队计数器
putCount
- 对消息序列化处理
- 按配置选择是异步发送到zk,还是同步操作
5.7.1 异步发送消息到ZK
如果是异步发送,则会调用org.apache.curator.framework.recipes.queue.DistributedQueue#doPutInBackground
方法:
private void doPutInBackground(final T item, String path, final MultiItem<T> givenMultiItem, byte[] bytes) throws Exception
{
BackgroundCallback callback = new BackgroundCallback()
{
@Override
public void processResult(CuratorFramework client, CuratorEvent event) throws Exception
{
if ( event.getResultCode() != KeeperException.Code.OK.intValue() )
{
return;
}
if ( event.getType() == CuratorEventType.CREATE )
{
synchronized(putCount)
{
putCount.decrementAndGet();
putCount.notifyAll();
}
}
putListenerContainer.forEach
(
new Function<QueuePutListener<T>, Void>()
{
@Override
public Void apply(QueuePutListener<T> listener)
{
if ( item != null )
{
listener.putCompleted(item);
}
else
{
listener.putMultiCompleted(givenMultiItem);
}
return null;
}
}
);
}
};
internalCreateNode(path, bytes, callback);
}
void internalCreateNode(String path, byte[] bytes, BackgroundCallback callback) throws Exception
{
client.create().withMode(CreateMode.PERSISTENT_SEQUENTIAL).inBackground(callback).forPath(path, bytes);
}
- 这个方法主要是装配了一个回调任务:
- 当新消息节点创建完成后
- 更新入队消息计数器
- 触发本地的监听器
- 当新消息节点创建完成后
- 实际的创建动作是由
internalCreateNode
方法完成的- 使用持久有序节点来创建消息节点
5.7.1 同步发送消息到ZK
如果是同步发送,则会调用org.apache.curator.framework.recipes.queue.DistributedQueue#doPutInForeground
方法:
private void doPutInForeground(final T item, String path, final MultiItem<T> givenMultiItem, byte[] bytes) throws Exception
{
client.create().withMode(CreateMode.PERSISTENT_SEQUENTIAL).forPath(path, bytes);
synchronized(putCount)
{
putCount.decrementAndGet();
putCount.notifyAll();
}
putListenerContainer.forEach
(
new Function<QueuePutListener<T>, Void>()
{
@Override
public Void apply(QueuePutListener<T> listener)
{
if ( item != null )
{
listener.putCompleted(item);
}
else
{
listener.putMultiCompleted(givenMultiItem);
}
return null;
}
}
);
}
同步阻塞调用:
- 创建节点
- 更新入队计数器
- 依次触发本地监听器
5.8 消费消息
如果需要消费消息,需要在初始化时指定消费者org.apache.curator.framework.recipes.queue.QueueConsumer
6. Builder
Curator为Queue准备了一个Builder模式:org.apache.curator.framework.recipes.queue.QueueBuilder
public class QueueBuilder<T>
{
private final CuratorFramework client;
private final QueueConsumer<T> consumer;
private final QueueSerializer<T> serializer;
private final String queuePath;
private ThreadFactory factory;
private Executor executor;
private String lockPath;
private int maxItems = NOT_SET;
private boolean putInBackground = true;
private int finalFlushMs = 5000;
static final ThreadFactory defaultThreadFactory = ThreadUtils.newThreadFactory("QueueBuilder");
static final int NOT_SET = Integer.MAX_VALUE;
6.1 构建一个普通队列
public DistributedQueue<T> buildQueue()
{
return new DistributedQueue<T>
(
client,
consumer,
serializer,
queuePath,
factory,
executor,
Integer.MAX_VALUE,
false,
lockPath,
maxItems,
putInBackground,
finalFlushMs
);
}
- 默认
- 启用
putInBackground
- 无界队列
maxItems = NOT_SET
- 关闭时如有消息需要投递,等待5秒
- 启用
7. 小结
Curator使用zk作为队列使用时
- 采用客户端拉取消息的方式,进行消费
- 采用拉取线程池,与消息消费线程池分别使用不同的线程池
- 资源隔离
- 将io线程与任务线程分开
- 带有优雅退出的机制
- 在关闭时,可以设置等待时间,等待消息投递完成
- 使用本地cache进行缓冲
- 使用版本号进行消息跟踪
- 减少io
- 在调度时,提供一些
protected
方法控制,方便子类定制调度策略