我刚读过RabbitMQ's Java API docs,发现它非常有用且直截了当。有关如何为发布/消费设置简单Channel
的示例非常易于理解。但这是一个非常简单/基本的例子,它给我留下了一个重要问题:如何设置1 + Channels
来发布/消费多个队列?
假设我有一个包含3个队列的RabbitMQ服务器:logging
,security_events
和customer_orders
。因此,我们要么需要一个Channel
才能发布/使用所有3个队列,或者更有可能拥有3个单独的Channels
,每个队列都专用于一个队列。
除此之外,RabbitMQ的最佳实践要求我们为每个消费者线程设置1 Channel
。对于这个例子,假设security_events
只有1个消费者线程,但logging
和customer_order
都需要5个线程来处理卷。所以,如果我理解正确,这是否意味着我们需要:
Channel
和1个消费者帖子,用于发布/消费security_events
;和Channels
和5个消费者主题用于发布/消费logging
;和Channels
和5个消费者主题用于发布/消费customer_orders
?如果我的理解在这里被误导,请先纠正我。无论哪种方式,一些厌倦战斗的RabbitMQ老手可以帮我“连接点”和一个不错的代码示例来设置符合我要求的发布商/消费者吗?提前致谢!
答案 0 :(得分:122)
我认为你有几个初步了解的问题。坦率地说,我对以下内容感到有点惊讶:both need 5 threads to handle the volume
。你怎么认出你需要那个确切的数字?你有什么保证5线程就够了吗?
RabbitMQ已经过调整和时间测试,所以这一切都与正确的设计有关 和有效的消息处理。
让我们尝试回顾一下问题并找到合适的解决方案。顺便说一下,消息队列本身不会提供任何保证,你有很好的解决方案。你必须了解自己在做什么,还要做一些额外的测试。
您肯定知道有很多可能的布局:
我将使用布局B
作为说明1
生产者N
消费者问题的最简单方法。既然你很担心吞吐量。顺便说一句,正如您所料,RabbitMQ表现得相当好(source)。请注意prefetchCount
,稍后我会解决:
因此,消息处理逻辑可能是确保您拥有足够吞吐量的正确位置。当然,每次需要处理消息时,您都可以跨越一个新线程,但最终这种方法会终止您的系统。基本上,您可以获得更多线程的延迟(如果需要,可以查看Amdahl's law。)
提示#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:注意消息处理开销
我会说这是明显的优化技术。您可能会发送小而易于处理的消息。整个方法是关于连续设置和处理的较小消息。大消息最终会起到一个糟糕的笑话,所以最好避免这种情况。
因此最好发送微小的信息,但是处理呢?每次提交工作都会产生管理费用。在传入消息率很高的情况下,批处理非常有用。
例如,假设我们有简单的消息处理逻辑,并且我们不希望每次处理消息时都有特定于线程的开销。为了优化非常简单的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
),但您必须快速完成所有操作。冗余处理步骤,重循环等可能会对性能产生很大影响。请参阅有趣的案例研究:
Improving Message Queue Throughput tenfold by choosing the right XML Parser
提示#4:连接和渠道管理
(source)
请注意,所有提示都完美地协同工作。如果您需要其他详细信息,请随时与我们联系。
完整的消费者示例(source)
请注意以下事项:
prefetchCount
可能非常有用:
此命令允许消费者选择预取窗口 指定准备好的未确认消息的数量 接收。通过将预取计数设置为非零值,代理 不会向消费者发送任何违反该消息的消息 限制。为了向前移动窗口,消费者必须承认 收到一条消息(或一组消息)。
示例:
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)
您可以使用线程和通道实现。所有你需要的是一种方法 对事物进行分类,即登录中的所有队列项,全部 来自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以获取有关线程部分的更多详细信息..