RabbitMQ示例:多个线程,通道和队列

时间:2013-08-30 10:49:03

标签: java multithreading rabbitmq messaging channel

我刚读过RabbitMQ's Java API docs,发现它非常有用且直截了当。有关如何为发布/消费设置简单Channel的示例非常易于理解。但这是一个非常简单/基本的例子,它给我留下了一个重要问题:如何设置1 + Channels来发布/消费多个队列?

假设我有一个包含3个队列的RabbitMQ服务器:loggingsecurity_eventscustomer_orders。因此,我们要么需要一个Channel才能发布/使用所有3个队列,或者更有可能拥有3个单独的Channels,每个队列都专用于一个队列。

除此之外,RabbitMQ的最佳实践要求我们为每个消费者线程设置1 Channel。对于这个例子,假设security_events只有1个消费者线程,但loggingcustomer_order都需要5个线程来处理卷。所以,如果我理解正确,这是否意味着我们需要:

  • 1 Channel和1个消费者帖子,用于发布/消费security_events;和
  • 5 Channels和5个消费者主题用于发布/消费logging;和
  • 5 Channels和5个消费者主题用于发布/消费customer_orders

如果我的理解在这里被误导,请先纠正我。无论哪种方式,一些厌倦战斗的RabbitMQ老手可以帮我“连接点”和一个不错的代码示例来设置符合我要求的发布商/消费者吗?提前致谢!

2 个答案:

答案 0 :(得分:122)

我认为你有几个初步了解的问题。坦率地说,我对以下内容感到有点惊讶:both need 5 threads to handle the volume。你怎么认出你需要那个确切的数字?你有什么保证5线程就够了吗?

  

RabbitMQ已经过调整和时间测试,所以这一切都与正确的设计有关   和有效的消息处理。

让我们尝试回顾一下问题并找到合适的解决方案。顺便说一下,消息队列本身不会提供任何保证,你有很好的解决方案。你必须了解自己在做什么,还要做一些额外的测试。

您肯定知道有很多可能的布局:

enter image description here

我将使用布局B作为说明1生产者N消费者问题的最简单方法。既然你很担心吞吐量。顺便说一句,正如您所料,RabbitMQ表现得相当好(source)。请注意prefetchCount,稍后我会解决:

enter image description here

因此,消息处理逻辑可能是确保您拥有足够吞吐量的正确位置。当然,每次需要处理消息时,您都可以跨越一个新线程,但最终这种方法会终止您的系统。基本上,您可以获得更多线程的延迟(如果需要,可以查看Amdahl's law。)

enter image description here

(见Amdahl’s law illustrated

提示#1:小心线程,使用ThreadPools(details

  

线程池可以描述为Runnable对象的集合   (工作队列)和正在运行的线程的连接。这些线程是   不断运行并正在检查新工作的工作查询。如果   他们执行这个Runnable还有新的工作要做。线程   类本身提供了一种方法,例如执行(Runnable r)添加新的   Runnable对象到工作队列。

public class Main {
  private static final int NTHREDS = 10;

  public static void main(String[] args) {
    ExecutorService executor = Executors.newFixedThreadPool(NTHREDS);
    for (int i = 0; i < 500; i++) {
      Runnable worker = new MyRunnable(10000000L + i);
      executor.execute(worker);
    }
    // This will make the executor accept no new threads
    // and finish all existing threads in the queue
    executor.shutdown();
    // Wait until all threads are finish
    executor.awaitTermination();
    System.out.println("Finished all threads");
  }
} 

提示#2:注意消息处理开销

我会说这是明显的优化技术。您可能会发送小而易于处理的消息。整个方法是关于连续设置和处理的较小消息。大消息最终会起到一个糟糕的笑话,所以最好避免这种情况。

enter image description here

因此最好发送微小的信息,但是处理呢?每次提交工作都会产生管理费用。在传入消息率很高的情况下,批处理非常有用。

enter image description here

例如,假设我们有简单的消息处理逻辑,并且我们不希望每次处理消息时都有特定于线程的开销。为了优化非常简单的CompositeRunnable can be introduced

class CompositeRunnable implements Runnable {

    protected Queue<Runnable> queue = new LinkedList<>();

    public void add(Runnable a) {
        queue.add(a);
    }

    @Override
    public void run() {
        for(Runnable r: queue) {
            r.run();
        }
    }
}

或者通过收集要处理的消息以稍微不同的方式做同样的事情:

class CompositeMessageWorker<T> implements Runnable {

    protected Queue<T> queue = new LinkedList<>();

    public void add(T message) {
        queue.add(message);
    }

    @Override
    public void run() {
        for(T message: queue) {
            // process a message
        }
    }
}

通过这种方式,您可以更有效地处理消息。

提示#3:优化邮件处理

尽管您知道可以并行处理消息(Tip #1)并减少处理开销(Tip #2),但您必须快速完成所有操作。冗余处理步骤,重循环等可能会对性能产生很大影响。请参阅有趣的案例研究:

enter image description here

Improving Message Queue Throughput tenfold by choosing the right XML Parser

提示#4:连接和渠道管理

  • 在现有连接上启动新频道涉及一个网络 往返 - 开始新的连接需要几个。
  • 每个连接都使用服务器上的文件描述符。频道没有。
  • 在一个频道上发布大型邮件会阻止连接 而它出去了。除此之外,多路复用相当透明。
  • 如果服务器是,则可以阻止正在发布的连接 超载 - 将发布和消费分开是一个好主意 连接
  • 准备好处理消息突发事件

source

请注意,所有提示都完美地协同工作。如果您需要其他详细信息,请随时与我们联系。

完整的消费者示例(source

请注意以下事项:

  • channel.basicQos(预取) - 正如您之前看到的prefetchCount可能非常有用:
      

    此命令允许消费者选择预取窗口   指定准备好的未确认消息的数量   接收。通过将预取计数设置为非零值,代理   不会向消费者发送任何违反该消息的消息   限制。为了向前移动窗口,消费者必须承认   收到一条消息(或一组消息)。

  • ExecutorService threadExecutor - 您可以指定正确配置的执行程序服务。

示例:

static class Worker extends DefaultConsumer {

    String name;
    Channel channel;
    String queue;
    int processed;
    ExecutorService executorService;

    public Worker(int prefetch, ExecutorService threadExecutor,
                  , Channel c, String q) throws Exception {
        super(c);
        channel = c;
        queue = q;
        channel.basicQos(prefetch);
        channel.basicConsume(queue, false, this);
        executorService = threadExecutor;
    }

    @Override
    public void handleDelivery(String consumerTag,
                               Envelope envelope,
                               AMQP.BasicProperties properties,
                               byte[] body) throws IOException {
        Runnable task = new VariableLengthTask(this,
                                               envelope.getDeliveryTag(),
                                               channel);
        executorService.submit(task);
    }
}

您还可以查看以下内容:

答案 1 :(得分:20)

如何设置1个以上的频道来发布/使用多个队列?

  

您可以使用线程和通道实现。所有你需要的是一种方法   对事物进行分类,即登录中的所有队列项,全部   来自security_events等的队列元素.Catagorization可以是   使用routingKey获得。

     

ie:每次向队列添加项目时,请指定路由   键。它将作为属性元素附加。通过这个,你可以得到   来自特定事件的值表示记录

以下代码示例说明了如何在客户端完成它。

<强>例如

使用路由键识别频道类型并检索类型。

  

例如,如果您需要获取有关Login类型的所有频道   那么您必须将路由密钥指定为登录或其他一些关键字   识别出来。

            Connection connection = factory.newConnection();
            Channel channel = connection.createChannel();

            channel.exchangeDeclare(EXCHANGE_NAME, "direct");

            string routingKey="login";

            channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes());

您可以查看here了解有关分类的详细信息..


线程部分

  

发布部分结束后,您可以运行线程部分..

在这部分中,您可以根据类别获取已发布的数据。即;路由密钥,在您的情况下是日志记录,security_events和customer_orders等。

查看示例以了解如何检索线程中的数据。

例如:

    ConnectionFactory factory = new ConnectionFactory();
    factory.setHost("localhost");
    Connection connection = factory.newConnection();
    Channel channel = connection.createChannel();
//**The threads part is as follows** 
 channel.exchangeDeclare(EXCHANGE_NAME, "direct");      
 String queueName = channel.queueDeclare().getQueue();
    // This part will biend the queue with the severity (login for eg:)
    for(String severity : argv){
              channel.queueBind(queueName, EXCHANGE_NAME, routingKey);
    }
    boolean autoAck = false;
    channel.basicConsume(queueName, autoAck, "myConsumerTag",
    new DefaultConsumer(channel) {
        @Override
        public void handleDelivery(String consumerTag,
                                Envelope envelope,
                                AMQP.BasicProperties properties,
                                byte[] body)
         throws IOException
     {
             String routingKey = envelope.getRoutingKey();
             String contentType = properties.contentType;
             long deliveryTag = envelope.getDeliveryTag();

             // (process the message components here ...)
             channel.basicAck(deliveryTag, false);
     }
 });
  

现在是一个处理队列中数据的线程   键入login(路由键)已创建。通过这种方式,您可以创建多个线程。   每个服务目的不同。

查看here以获取有关线程部分的更多详细信息..