上节我们介绍了RabbitMQ的工作队列,这一节先会对RabbitMQ的几种交换机做个大概的介绍,然后会介绍一下fanout(扇形)类型的exchange,并通过代码示例达到Publish/Subscribe(发布订阅)也就是广播的效果。
我们之前的例子当中,将exchange的名字设为了空字符串,貌似都是将消息直接发送给了Queue,实际上也是发送给了交换机的,只不过这个exchange的名字是个空字符串,是RabbitMQ的默认的交换机。RabbitMQ中消息传递模型的核心思想是生产者永远不会将任何消息直接发送到队列。实际上,生产者通常甚至不知道消息是否会被传递到任何队列。
如果对于交换机、routing key、绑定、绑定key这些概念不太了解,可以先看一下《(2)RabbitMQ基础概念及工作流程详解》的介绍,这里不做重复介绍了。
生产者发送消息时指定交换机名称、类型和routing key,消息发送到指定的交换机之后,exchange会根据交换机类型和routing key将消息路由到绑定的队列上面。
交换机主要包括如下4种类型:
另外RabbitMQ默认定义一些交换机:默认匿名交换机、amq.* exchanges。还有一类特殊的交换机:Dead Letter Exchange(死信交换机)
fanout类型的Exchange路由规则非常简单,它会把所有发送到该Exchange的消息路由到所有与它绑定的Queue中,所以此时routing key是不起作用的。
上图中,生产者(P)发送到Exchange(X)的所有消息都会路由到图中的两个Queue,并最终被两个消费者(C1与C2)消费。
direct类型的Exchange路由规则也很简单,它会把消息路由到那些binding key与routing key完全匹配的Queue中。
以上图的配置为例,我们以routingKey=”error”发送消息到Exchange,则消息会路由到Queue1(amqp.gen-S9b…,这是由RabbitMQ自动生成的Queue名称)和Queue2(amqp.gen-Agl…);如果我们以routingKey=”info”或routingKey=”warning”来发送消息,则消息只会路由到Queue2。如果我们以其他routingKey发送消息,则消息不会路由到这两个Queue中。
前面讲到direct类型的Exchange路由规则是完全匹配binding key与routing key,但这种严格的匹配方式在很多情况下不能满足实际业务需求。topic类型的Exchange在匹配规则上进行了扩展,它与direct类型的Exchage相似,也是将消息路由到binding key与routing key相匹配的Queue中,但这里的匹配规则有些不同,它约定:
以上图中的配置为例,routingKey=”quick.orange.rabbit”的消息会同时路由到Q1与Q2,routingKey=”lazy.orange.fox”的消息会路由到Q1,routingKey=”lazy.brown.fox”的消息会路由到Q2,routingKey=”lazy.pink.rabbit”的消息会路由到Q2(只会投递给Q2一次,虽然这个routingKey与Q2的两个bindingKey都匹配);routingKey=”quick.brown.fox”、routingKey=”orange”、routingKey=”quick.orange.male.rabbit”的消息将会被丢弃,因为它们没有匹配任何bindingKey。
headers类型的Exchange不依赖于routing key与binding key的匹配规则来路由消息,而是根据发送的消息内容中的headers属性进行匹配。
在绑定Queue与Exchange时指定一组键值对;当消息发送到Exchange时,RabbitMQ会取到该消息的headers(也是一个键值对的形式),对比其中的键值对是否完全匹配Queue与Exchange绑定时指定的键值对;如果完全匹配则消息会路由到该Queue,否则不会路由到该Queue。
该类型的Exchange用的没有上面三种广泛,所以本次博客中就不单独进行示例介绍了。
默认匿名交换机(default exchange):实际上是一个由RabbitMQ预先声明好的名字为空字符串的直连交换机(direct exchange),所以我们前面发送消息时设置的exchange的name都是空字符串。它有一个特殊的属性使得它对于简单应用特别有用处:那就是每个新建队列(queue)都会自动绑定到默认交换机上,绑定的路由键(routing key)名称与队列名称相同。如:当你声明了一个名为”hello_queue”的队列,RabbitMQ会自动将其绑定到默认交换机上,绑定(binding)的路由键名称也是为”hello_queue”。因此,当携带着名为”hello_queue”的路由键的消息被发送到默认交换机的时候,此消息会被默认交换机路由至名为”hello_queue”的队列中。即默认交换机看起来貌似能够直接将消息投递给队列。
例如我们之前用 channel.basicPublish("", “hello_queue”, null, message.getBytes()) 的方式发送消息,空字符串就是默认匿名交换机的名称, hello_queue就是routing key。
类似amq.*的名称的交换机:这些是RabbitMQ默认创建的交换机。这些队列名称被预留做RabbitMQ内部使用,不能被应用使用,否则抛出403 (ACCESS_REFUSED)错误
通过查看AMQP default 交换机详情中的binding,我们也能看到对应的说明如下:
上面是通过RabbitMQ的web管理台看到的,我们也可以通过命令:rabbitmqctl list_exchanges 进行查看,可以看到第二行的一个交换机名字是空字符串,类型是direct
[root@wkp4 ~]# rabbitmqctl list_exchanges
Listing exchanges
amq.direct direct
direct
amq.rabbitmq.log topic
amq.headers headers
amq.fanout fanout
amq.topic topic
amq.rabbitmq.trace topic
amq.match headers
Dead Letter Exchange(死信交换机):在默认情况,如果消息在投递到交换机时,交换机发现此消息没有匹配的队列,则这个消息将被悄悄丢弃。为了解决这个问题,RabbitMQ中有一种交换机叫死信交换机。当消费者不能处理接收到的消息时,将这个消息重新发布到另外一个队列中,等待重试或者人工干预。这个过程中的exchange和queue就是所谓的”Dead Letter Exchange 和 Queue”。
项目GitHub地址 https://github.com/RookieMember/RabbitMQ-Learning.git。这节我们先介绍一下fanout类型的交换机,实现一下Publish/Subscribe(发布订阅)的效果,即一条消息发送给多个消费者,之前的work queues都是只发送给一个消费者。下面是消息发送者代码,我们可以看到生产者只是声明了交换机,然后将消息发送到了交换机中,整个过程当中并没有设置队列相关的参数。
注意:由于使用fanout类型的交换机时,routing key是不起作用的,所以代码中生产者发送消息、消费者中交换机与队列绑定时,routing key设置的都是空字符串,你可以将routing key改为任意值,你发现消费者都还是可以收到消息的。
package cn.wkp.rabbitmq.newest.exchange.fanout;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import cn.wkp.rabbitmq.util.ConnectionUtil;
public class Send {
private final static String EXCHANGE_NAME = "fanout_exchange";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
// 从连接中创建通道
Channel channel = connection.createChannel();
// 声明交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
// 消息内容
String message = "这是一条fanout类型交换机消息";
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
System.out.println("Sent message:" + message);
//关闭通道和连接
channel.close();
connection.close();
}
}
上面的生产者代码声明交换机时使用了重载的方法channel.exchangeDeclare(String exchange, BuiltinExchangeType type),其中BuiltinExchangeType是个枚举类,列出了所有类型的交换机(该type参数也可以使用字符串"fanout"的形式),其源码如下所示。
public enum BuiltinExchangeType {
DIRECT("direct"), FANOUT("fanout"), TOPIC("topic"), HEADERS("headers");
private final String type;
BuiltinExchangeType(String type) {
this.type = type;
}
public String getType() {
return type;
}
}
完整方法为:channel.exchangeDeclare(String exchange, String type, boolean durable, boolean autoDelete,Map
消费者1代码如下:
package cn.wkp.rabbitmq.newest.exchange.fanout;
import java.io.IOException;
import com.rabbitmq.client.AMQP.BasicProperties;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import cn.wkp.rabbitmq.util.ConnectionUtil;
public class Recv1 {
private final static String EXCHANGE_NAME = "fanout_exchange";
private final static String QUEUE_NAME = "fan_queue1";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定队列到交换机
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
//指该消费者在接收到队列里的消息但没有返回确认结果之前,它不会将新的消息分发给它。
channel.basicQos(1);
// 定义队列的消费者
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body)
throws IOException {
System.out.println("消费者1收到消息:" + new String(body));
//消费者手动发送ack应答
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
// 监听队列
channel.basicConsume(QUEUE_NAME, false, consumer);
}
}
消费者2代码如下(注意两个消费者队列名称不同):
package cn.wkp.rabbitmq.newest.exchange.fanout;
import java.io.IOException;
import com.rabbitmq.client.AMQP.BasicProperties;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import cn.wkp.rabbitmq.util.ConnectionUtil;
public class Recv2 {
private final static String EXCHANGE_NAME = "fanout_exchange";
private final static String QUEUE_NAME = "fan_queue2";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定队列到交换机
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
//指该消费者在接收到队列里的消息但没有返回确认结果之前,它不会将新的消息分发给它。
channel.basicQos(1);
// 定义队列的消费者
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body)
throws IOException {
System.out.println("消费者2收到消息:" + new String(body));
//消费者手动发送ack应答
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
// 监听队列
channel.basicConsume(QUEUE_NAME, false, consumer);
}
}
运行结果如下所示:
Sent message:这是一条fanout类型交换机消息
消费者1收到消息:这是一条fanout类型交换机消息
消费者2收到消息:这是一条fanout类型交换机消息
关于fanout类型的交换机就先介绍到这里,下一节将会介绍一下direct类型的交换机。