发布/订阅(Publish/Subscribe)
在上一篇教程中,我们学习了创建工作队列。这种情况假定工作队列中的每一个任务只会被分配给一个工作者(worker)。在这篇教程中,我们要做的事情完全相反——我们会把消息发送给多个消费者。这种模式被叫做 “发布/订阅模式”。
我们将通过建立一个简单的日志系统来解释这种模式。系统由两个程序组成——第一个程序负责发布日志信息,第二程序接收并打印出这些信息。
在这个日志系统中,每一个运行中的接收者进程都会收到消息。这种情况下,我们可以实现不同的消费者执行不同的功能,有的可以直接将日志记录到磁盘,同时,其他消费者可能在向屏幕中输出日志信息。
交换机(exchange)
在前面的教程中,我们都是通过queue发送和接收消息。现在是时候介绍一下完整的消息模型了。
我们这里快速回顾一下前面的教程所做的事情:
生产者程序负责发送消息。
队列负责存储消息。
消费者程序负责接收消息。
在RabbitMQ的消息模型中,核心思想是生产者不会直接向队列发送任何消息。通常,生产者甚至都不知道消息是否真的发送到了某个队列中。
相反,消费者只会向交换机(exchange)发送消息。交换机其实是一个非常简单的东西。一方面它会从消费者方向接收信息,另一方面把这些消息推送到队列。交换机必须明确的知道如何处理接收到的消息。这条消息该被推送到某个特定的队列吗?或者是多个队列中,再或者被丢弃掉,这些都是通过交换机类型(exchange type)去定义的。
有多种可用的交换机类型:direct, topic, headers和fanout。我们将重点关注最后一个——fanout。我们来创建一个fanout类型的交换机,名字叫做logs。
channel.exchangeDeclare("logs", "fanout");
fanout类型交换机很简单,它会把接受到的所有消息发送给所有队列。这正是我们在日志系统中需要的。
在前面的教程中我们并没有提及交换机的内容,但还是能够发消息,其实这是因为我们使用了默认的exchange,通过ide的提示我们看到我们只是定义了一个没有名字的交换机。
现在我们既然定义了一个exchange,那么就可以这么写代码了:
channel.basicPublish( "logs", "", null, message.getBytes());
临时队列
大家应该还记得在之前几篇文章中,我们使用的队列都是有名字的(hello和task_queue)。为队列命名对我们来说是十分重要的一件事情,因为我们需要将消费者指向相同的队列。所以让生产者和消费者共用一个队列的关键就在于队列名字。
但是对于我们的日志系统来说,情况却并非如此。因为我们想要监听所有的消息,而且我们也只想关注目前正在传输的消息流,而不是那些已经失效的。解决这些问题,我们需要做两件事。
第一,无论何时连接到RabbitMQ时,我们需要一个全新的空队列。要做的这一点我们可以每次启动时给队列创建随机名称,或者最好,让服务器帮我们选择一个随机名称。
第二,当断开消费者连接后,队列应该自动删除。
如果不向queueDeclare()方法中传递任何参数,那么我们就会创建一个非持久化,排他的(?),自动删除的,使用随机生成名称的队列。
String queueName = channel.queueDeclare().getQueue();
这样就能得到一个随机的队列名字。例如,amq.gen-JzTY20BRgKO-HjmUJj0wLg.
绑定(Bindings)
现在我们已经有了一个fanout类型的exchange和一个queue,还需要告诉exchange把消息发送到哪些queue中,这种exchange和queue的关系就称之为bindings。
channel.queueBind(queueName, "logs", "");
现在,exchange就会向queue中发送消息了
全部合并
从图中可以看到,发送者相较于之前来说没有什么变化。最重要的变化是现在通过交换机发送消息到队列。
public class EmitLog {
private static final String EXCHANGE_NAME = "logs";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
String message = argv.length < 1 ? "info: Hello World!" :
String.join(" ", argv);
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + message + "'");
}
}
}
通过代码可以看到,在建立了connection之后我们又声明了exchange。这一步是必须的,因为不能够像一个不存在的exchange发布消息。
如果没有queue绑定到exchange的话消息就会丢失,如果没有消费者监听的话,消息就会被丢弃。
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;
public class ReceiveLogs {
private static final String EXCHANGE_NAME = "logs";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
String queueName = channel.queueDeclare().getQueue();
channel.queueBind(queueName, EXCHANGE_NAME, "");
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
}
}
你可以打印出队列的名字,运行代码后通过控制台或者命令行可以更清晰的看到,多个消费者虽然订阅了不同的队列,都接收到了发送的消息。
以上就是RabbitMQ发布/订阅模式部分的内容了,相对于之前的内容,引入了exchange的概念,exchange作为向队列分发消息的部分,将exchange设置为fanout类型,那么就会向所有binding的队列发送消息,对应所有的消费者都能够接收到消息。