[Curator] Distributed Queue 的使用与分析

Curator 使用 ZooKeeper 实现了一个分布式队列,该队列支持消息的顺序消费及安全消费模式。队列采用客户端主动拉取消息的方式进行消费,并通过线程池分离 I/O 和任务处理,支持优雅关闭,等待消息投递完成。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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:

  1. ZooKeeper has a 1MB transport limitation. In practice this means that ZNodes must be relatively small. Typically, queues can contain many thousands of messages.
  1. 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.
  1. 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.
  1. ZooKeeper can start to perform badly if there are many nodes with thousands of children.
  1. The ZooKeeper database is kept entirely in memory. So, you can never have more messages than can fit in memory.

虽然Curator提供了几种zk的队列方案,但仍然不建议使用zk作为队列来使用,因为:

  1. zk对于传输数据有一个 1MB 的大小限制。
  • 这就意味着实际中zk节点ZNodes必须设计的很小
  • 但实际中队列通常都存放着数以千计的消息
  1. 如果有很多大的ZNodes,那会严重拖慢的zk启动过程。
  • 包括zk节点之间的同步过程
  • 如果正要用zk当队列,最好去调整initLimitsyncLimit
  1. 如果一个ZNode过大,也会导致清理变得困难
  • 也会导致getChildren()方法失败
  • Netflix不得不设计一个特殊的机制来处理这个大体积的nodes
  1. 如果zk中某个node下有数千子节点,也会严重拖累zk性能
  2. 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描述
04格式版本,当前版本:0x00010001
41指令:0x01 = 消息, 0x02 = 数据结束
54消息字节长度
9n消息序列化后的字节
9 n...下一组(指令-长度-序列化字节),直到数据结束

4. 错误处理

QueueConsumer类继承了ConnectionStateListener。当队列启动完成,会自动添加监听器。 不论如何,使用DistributedQueue必须要注意zk连接状态的变化。

如果中断(SUSPENDED)状态发生,实例必须认为队列没有更新,直到重连成功(SUSPENDED)。 如果连接丢失(LOST),则应该认定队列彻底不可用。

5. 源码分析

5.1 类图

先看看Curator Queue的几个核心对象的关系: image

可以看出:

  • 所有的队列都实现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

可以看到,基本都是简单赋值,但是有几个属性经过了一些处理:

  • 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;
                    }
                }
            );
    }
}
  1. 先使用CAS操作更新状态
  2. 创建队列的zk节点
  3. 如果需要,创建对应分布式锁的zk节点
  4. 如果仅仅是生产者角色,并且设定为有界队列
    • 启动子节点缓存
  5. 如果不仅是生产者
    • 则触发一个异步操作调用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();
    }
}
  1. 使用观察者模式(监听器),再必要时恢复子节点信息
  2. 使用回调处理队列的子节点信息
  3. 使用一个版本号对子节点列表进行本地缓存
    • 使用不可变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. 获取子节点缓存
    1. 如果版本相同,说明本地缓存比较旧需要等待同步后才能返回新数据
      • 启动时获取版本-1
      • ChildrenCache.Data的默认版本是0
      • 所以启动时,会直接返回初始化的ChildrenCache.Data
  2. 对子节点进行字典排序
  3. 如果有子节点信息
    • 使用getDelay根据当前子节点信息获取下一次获取新消息的延迟时间
      • 子类可以覆盖这个方法,进行队列的扩展
  4. 调用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
    • 当作闭锁使用

为每一个子节点调度一个异步任务:

  1. 如果指定了lockPath,则使用加锁的方式,安全的处理消息节点
  2. 否则,以普通的方式处理消息节点
  3. 处理完成后,释放信号量

最后,利用信号量阻塞,等待当前版本的子节点全部处理完成

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事务)执行:
    1. 删除原队列中的消息节点
    2. 将消息数据重新写入另一个队列
      • 具体是哪个队列,由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 小结

重新整理一下启动过程:

  1. 更新启动状态
  2. 创建队列路径
  3. 如果需要,创建队列消息消费时锁的路径
  4. 如果指定了消费者,或者是一个有界队列,则启动子节点缓存(childrenCache)
    1. 添加监听器
      • 用以在各种意外情况下,自动同步缓存
      • 拉取消息节点
      • notifyAll()通知等待的消费者
  5. 如果指定了消费者,则异步启动轮询任务拉取消息节点
    1. 阻塞获取队列消息节点
    2. getDelay方法,进行延迟(定制消息的消费策略)
    3. 处理消息
      1. 对消息的消费进行一定的过滤和调度
        • 最小刷新率
        • 延迟
      2. 为每一个可以处理的消息节点创建一个异步处理任务
        1. 拉取消息节点数据
          • 如需安全消费,则使用临时节点进行占位
        2. 调用consumer消费消息
        3. 按照处理结果进行消息收尾工作
          • 重入队列
            • 可以通过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状态后:

  1. 如果配置了延迟毫秒数
    • 则调用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;
}
  1. 如果指定了等待时间,则进行判定是否需要阻塞等待
    • 如果已经超过最大数量,则会触发一次同步(再次拉取zk中的消息节点)
    • 如果,版本(消息数据跟踪版本)没有变化,则判定消息进入队列失败
  2. 将消息item包装成org.apache.curator.framework.recipes.queue.MultiItem
  3. 更新入队计数器putCount
  4. 对消息序列化处理
  5. 按配置选择是异步发送到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);
}
  • 这个方法主要是装配了一个回调任务:
    1. 当新消息节点创建完成后
      • 更新入队消息计数器
    2. 触发本地的监听器
  • 实际的创建动作是由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;
            }
        }
    );
}

同步阻塞调用:

  1. 创建节点
  2. 更新入队计数器
  3. 依次触发本地监听器

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方法控制,方便子类定制调度策略

转载于:https://my.oschina.net/roccn/blog/1154549

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值