在前面的工作队列中我们创建了工作队列。工作队列背后的假设是每一项任务只会明确分配给一个工作者。
在本节中我们将学RabbitMQ的发布、订阅模式,即一条消息会同时投递所有订阅者。
一、Exchanges 交换器
RabbitMQ消息传递模型的核心思想是,生产者永远不会将任何消息直接发送到队列。实际上,通常生产者甚至不知道消息是否会被传递到任何队列。
在RabbitMQ中,生产者只能向exchange(交换器)发送消息。交换器负责接收生产者的消息,并将其转发到消费者。所以交换器本身需要知道具体的分发规则,例如是否要将消息添加到特定的队列?是否添加到多个队列?是否丢弃消息等等。
1.1 交换机类型(Exchange Type)
RabbitMQ中有多种交换机类型:
- Direct exchange(直连交换机):根据消息携带的路由键(routing key)将消息投递到对应的队列。
- Fanout exchange(扇型交换机):将消息路由到该交换机绑定的所有队列。
- Topic exchange(主题交换机):队列通过路由键绑定到交换机上,交换机根据消息里的路由值,将消息路由给一个或多个绑定队列。
- Headers exchange(头交换机):类似主题交换机,但是头交换机使用多个消息属性来代替路由键建立路由规则。当”x-match”为“any”时,消息头的任意一个值被匹配就可以满足条件;当”x-match”设置为“all”的时候,就需要消息头的所有值都匹配成功
- 默认存在的交换机:实际上是一个由RabbitMQ预先声明好的名字为空字符串的直连交换机(direct exchange),每个新建队列(queue)都会自动绑定到默认交换机上,绑定的路由键(routing key)名称与队列名称相同。
- Dead Letter Exchange(死信交换机):在默认情况,如果消息在投递到交换机时,交换机发现此消息没有匹配的队列,则这个消息将被悄悄丢弃。为了解决这个问题,RabbitMQ中有一种交换机叫死信交换机。当消费者不能处理接收到的消息时,将这个消息重新发布到另外一个队列中,等待重试或者人工干预。这个过程中的exchange和queue就是所谓的”Dead Letter Exchange 和 Queue”
1.2 交换机使用
我们使用如下语句来声明交换机:
channel.exchangeDeclare("logs","fanout");//logs是交换机的名字,fanout是交换机类型
我们可以通过如下命令来列出RabbitMQ Server上的所有交换机:
root@VM-32-73-ubuntu:/home/ubuntu# sudo rabbitmqctl list_exchanges
Listing exchanges ...
direct
amq.direct direct
amq.fanout fanout
amq.headers headers
amq.match headers
amq.rabbitmq.log topic
amq.rabbitmq.trace topic
amq.topic topic
其中amp开头的队列都是RabbitMQ自动创建的,用于内部使用,外部无法调用。
在前面的章节中我们是这样发送消息的:
channel.basicPublish("", "hello", null, message.getBytes());
其中第一个参数就代表交换机的名字,为空则表示使用默认交换机(1.1 第5点),路由键被设置为队列名。
二、临时队列(Temporary queues)
在前面的例子中,我们声明队列时都指定了队列名(hello、task_queue),但这并不是必须的。如果不指定队列名,则代表使用临时队列:
String queueName = channel.queueDeclare().getQueue();
临时队列有如下特性:
- 在连接建立时创建,并被分配一个随机名(如amq.gen-JzTY20BRgKO-HjmUJj0wLg)。
- 在连接断开后自动销毁。
三、绑定 bindings
交换机和队列都是独立的,要让交换机将消息发送到特定的队列,就需要将它们绑定(binding),我们使用如下语句建立绑定关系:
channel.queueBind(queueName, "logs", "");//queueName代表队列名,logs代表交换机的名字,最后一个参数为路由键(routing key)
我们可以用如下命令查看绑定关系:
root@VM-32-73-ubuntu:/home/ubuntu# rabbitmqctl list_bindings
Listing bindings ...
exchange hello queue hello []
exchange task_queue queue task_queue []
四、整体代码
EmitLog.java 生产者
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 + "'");
}
}
}
ReceiveLogs.java 消费者
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 -> { });
}
}
总结
参考:
https://blog.youkuaiyun.com/hry2015/article/details/79118804