参考教程:
相关文章:
写在开头:本文为个人学习笔记,内容比较随意,夹杂个人理解,如有错误,欢迎指正。
前言
在此前的文章中,介绍了AMQP协议,而rabbitmq正是该协议的实现,本文我们就来介绍下rabbitmq中的Exchange模型。(建议先学习下rabbitmq的Hello World样例《RabbitMQ(一):Hello World程序》,明确生产者、消费者、队列以及交换机的概念及关系)
目录
一、Fanout
1、定义
Fanout模式即广播,每个发到fanout类型交换器的消息都会分到所有绑定队列上去。fanout交换器不处理路由键,只是简单的将队列绑定到交换器上,每个发送到交换器的消息都会被转发到与该交换器绑定的所有队列上。很像子网广播,每台子网内的主机都获得了一份复制的消息。fanout类型转发消息是最快的。
2、样例
假设我们要创建一个简单的日志系统。它由两个程序组成:第一个将会输出日志消息,第二个将会接受并打印出日志消息。在这个日志系统中,每一个接收程序(消费者)都会收到所有的消息,其中一个消费者将消息直接保存到磁盘中,而另一个消费者则将日志输出到控制台。
首先申明交换器
// 第一个参数表示交换器的名称,第二个参数表示发布订阅模型
channel.exchangeDeclare("logs", "fanout");
然后是消息发布,这里由于我们使用的是广播类型,所以路由规则可以为空,此时所有与这个交换器绑定的队列都能接收到这条消息。
// 四个参数分表表示交换器名称,路由规则routing key,消息参数以及消息内容
channel.basicPublish( "logs", "", null, message.getBytes());
最后是消费者申明队列并绑定交换器,这里我们使用临时队列(临时队列的好处是该消费者独享,且消费者下线后自动删除)。
// 获取一个随机名称的临时对立
String queueName = channel.queueDeclare().getQueue();
// 绑定临时队列与交换器,由于是广播模式所以路binding key为空
channel.queueBind(queueName, "logs", "");
整合到一起后就是这样的(注意实际上需要先启动消费者再启动生产者,不然消息无处投递会被自动删除,这里是为了方便理解所以先展示了生产者后展示的消费者)
生产者申明一个广播类型的路由器并发送消息。
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
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");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
//声明路由以及路由的类型
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
String message = "msg...";
//发布消息
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + message + "'");
//关闭连接和通道
channel.close();
connection.close();
}
}
消费者需要将获取到的队列与交换器绑定,然后就可以获取消息了
import com.rabbitmq.client.*;
import java.io.IOException;
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, BuiltinExchangeType.FANOUT);
//声明一个随机名字的队列
String queueName = channel.queueDeclare().getQueue();
//绑定队列到路由器上
channel.queueBind(queueName, EXCHANGE_NAME, "");
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
//开始监听消息
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body, "UTF-8");
System.out.println(" [x] Received '" + message + "'");
}
};
channel.basicConsume(queueName, true, consumer);
}
}
首先运行两个消费者实例ReceiveLogs.java,然后运行生产者EmitLog.java。可以看到两个消费者都收到了同样的消息,这就是广播的发布订阅模型。
//生产者
[x] Sent 'msg...'
//消费者1
[*] Waiting for messages. To exit press CTRL+C
[x] Received 'msg...'
//消费者2
[*] Waiting for messages. To exit press CTRL+C
[x] Received 'msg...'
这里补充一下,一个队列中的一条消息是只能被一个消费者消费的,这里两个消费者消费了同样的消息是因为连接了两个不同的队列,如果是两个消费者连接了同一个队列那么每个消息就只能被其中的一个所消费。
二、Direct
1、定义
Direct模式即单播,消息中的路由键(routing-key) 如果和Binding中的Binding key一致,交换器就发到对应的队列中。
上一节介绍的Fanout路由器缺少灵活性,它只是将消息广播给所有的队列。所以,我们用Direct路由器来替换它。Direct路由器背后的路由算法很简单:只有当消息的路由键routing key与队列的绑定键binding key完全匹配时,该消息才会进入该队列。
广播模式中无论是生产者发送消息时的路由键routing key与消费者绑定队列与交换器时的绑定键binding key都为空,到了单播模式这里为了将不同的队列区分开来,我们需要设置这两个值。
上图中,直接路由器X与两个队列绑定。第一个队列以绑定键orange来绑定,第二个队列以两个绑定键black和green和路由器绑定。按照这种设置,路由键为orange的消息在发布给路由器后,将会被路由到队列Q1,路由键为black或者green的消息将会路由到队列Q2。(可以从上图得知,一个队列和交换器的绑定规则是可以有多个的)
另外多个队列以相同的绑定键binding key绑定到同一个Exchange上,也是可以的。按照这种方式设置的话,直接路由器就会像Fanout路由器一样,将消息广播给所有符合路由规则的队列。一个路由键为black的消息将会发布到队列Q1和Q2。
2、样例
在广播模式中,我们创建了一个简单的日志系统。我们可以将日志消息广播给所有的接收者(消费者)。现在我们想添加一个功能:仅仅订阅一部分消息。比如,我们可以直接将关键的错误类型日志消息保存到日志文件中,还可以同时将所有的日志消息打印到控制台。
我们使用Direct路由器来代替上个教程中的Fanout路由器。同时,我们为日志设置严重级别,并将此作为路由键。这样,接收者(消费者)就可以选择性地接收日志消息。
这一次我们要将路由器申明为Direct模式,并在发送消息的时候设置路由键routing key
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
// 当使用Direct模式时routing key路由键就不能为空了,因为要涉及到和绑定键bingd key的完全匹配
channel.basicPublish(EXCHANGE_NAME, severity, null, message.getBytes());
消费者在绑定路由器与队列时需要设置绑定键bingd key,只有当路由键和绑定键完全相同时这条消息才会发送给这个队列。这里我们通过遍历的方式实现了多重绑定。
String queueName = channel.queueDeclare().getQueue();
// 遍历类型集合进行多重绑定(argv集合包括info、warning 、error)
for(String severity : argv){
channel.queueBind(queueName, EXCHANGE_NAME, severity);
}
实现的效果如下图所示
完整的生产者代码如下,和广播模式唯一的区别就是设置了路由键routing key
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
public class EmitLogDirect {
private static final String EXCHANGE_NAME = "direct_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, BuiltinExchangeType.DIRECT);
String severity = "info";
String message = ".........i am msg.........";
//发布消息
channel.basicPublish(EXCHANGE_NAME, severity, null, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + severity + "':'" + message + "'");
channel.close();
connection.close();
}
}
然后是消费者的代码,这里我们做了多重绑定
import com.rabbitmq.client.*;
import java.io.IOException;
public class ReceiveLogsDirect {
private static final String EXCHANGE_NAME = "direct_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, BuiltinExchangeType.DIRECT);
//声明队列
String queueName = channel.queueDeclare().getQueue();
//定义要监听的级别
String[] severities = {"info", "warning", "error"};
//根据绑定键绑定
for (String severity : severities) {
channel.queueBind(queueName, EXCHANGE_NAME, severity);
}
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body, "UTF-8");
System.out.println(" [x] Received '" + envelope.getRoutingKey() + "':'" + message + "'");
}
};
channel.basicConsume(queueName, true, consumer);
}
}
我们先启动一个消费者实例(ReceiveLogsDirect.java),然后将其中的要监听的级别改为String[] severities = {"error"};,再启动另一个消费者实例。此时,这两个消费者都开始监听了,一个监听所有级别的日志消息,另一个监听error日志消息。
然后,启动生产者(EmitLogDirect.java),之后将String severity = "info";中的info,分别改为warning、error后运行。
//生产者
[x] Sent 'warning':'.........i am msg.........'
[x] Sent 'info':'.........i am msg.........'
[x] Sent 'error':'.........i am msg.........'
//消费者1
[*] Waiting for messages. To exit press CTRL+C
[x] Received 'info':'.........i am msg.........'
[x] Received 'error':'.........i am msg.........'
[x] Received 'warning':'.........i am msg.........'
//消费者2
[*] Waiting for messages. To exit press CTRL+C
[x] Received 'error':'.........i am msg.........'
可以看到这一次队列按照不同的消息类型进行了区分,这就是Direct模式。
三、Topic
1、定义
有选择的广播,topic交换器通过模式匹配分配消息的路由键属性,将路由键和某个模式进行匹配,此时队列需要绑定到一个模式上。它将路由键和绑定键的字符串分成单词,这些单词用点隔开(a.b)。它同样也会识别两个通配符:符号:# 匹配0或者多个单词,*匹配一个单词。
发送到Topic路由器的消息的路由键routing_key不能任意给定:它必须是一些单词的集合,中间用点号.分割。这些单词可以是任意的,但通常会体现出消息的特征。一些有效的路由键示例:stock.usd.nyse,nyse.vmw,quick.orange.rabbit。这些路由键可以包含很多单词,但路由键总长度不能超过255个字节。
绑定键binding key也必须是这种形式。Topic路由器背后的逻辑与Direct路由器类似:以特定路由键发送的消息将会发送到所有绑定键与之匹配的队列中。但绑定键有两种特殊的情况:
*(星号)仅代表一个单词,#(井号)代表任意个单词
对于上图的例子,我们将会发送描述动物的消息。这些消息将会以由三个单词组成的路由键发送。路由键中的第一个单词描述了速度,第二个描述了颜色,第三个描述了物种:<speed>.<colour>.<species>。
我们创建了三个绑定,Q1的绑定键为*.orange.*,Q2的绑定键有两个,分别是*.*.rabbit和lazy.#。上述绑定关系可以描述为:
(1)Q1关注所有颜色为orange的动物。
(2)Q2关注所有的rabbit,以及所有的lazy的动物。
如果一个消息的路由键是quick.orange.rabbit,那么Q1和Q2都可以接收到,路由键是lazy.orange.elephant的消息同样如此。但是,路由键是quick.orange.fox的消息只会到达Q1,路由键是lazy.brown.fox的消息只会到达Q2。注意,路由键为lazy.pink.rabbit的消息只会到达Q2一次,尽管它匹配了两个绑定键。路由键为quick.brown.fox的消息因为不和任意的绑定键匹配,所以将会被丢弃。
但如果我们发送一个路由键只有一个单词或者四个单词的消息,像orange或者quick.orange.male.rabbit,这样的话,这些消息因为不和任意绑定键匹配,都将会丢弃。但是,lazy.orange.male.rabbit消息因为和lazy.#匹配,所以会到达Q2,尽管它包含四个单词。
2、样例
上一节中我们使用direct路由器替代了Fanout路由器,从而可以选择性地接收日志。尽管使用Direct路由器给我们的日志系统带来了改进,但仍然有一些限制:不能基于多种标准进行路由。如果我们能监听来自corn的错误日志,同时也监听kern的所有日志,那么我们的日志系统就会更加灵活。
为了实现这个功能,我们需要使用Topic路由器。我们将会在我们的日志系统中使用主题路由器Topic exchange,并假设所有的日志消息以两个单词kern、critical为路由键。
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
public class EmitLogTopic {
private static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] argv) {
Connection connection = null;
Channel channel = null;
try {
//建立连接和通道
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
connection = factory.newConnection();
channel = connection.createChannel();
//声明路由器和路由器类型
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
//定义路由键和消息
//routingKey依次调整为kern.critical、kern.info、kern.warn、auth.critical、cron.warn、cron.critical
String routingKey = "kern.critical";
String message = "msg.....";
//发布消息
channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + routingKey + "':'" + message + "'");
} catch (Exception e) {
e.printStackTrace();
} finally {
if (connection != null) {
try {
connection.close();
} catch (Exception ignore) {
}
}
}
}
}
消费者中使用通配符作为bingingKey
import com.rabbitmq.client.*;
import java.io.IOException;
public class ReceiveLogsTopic {
private static final String EXCHANGE_NAME = "topic_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, BuiltinExchangeType.TOPIC);
String queueName = channel.queueDeclare().getQueue();
// 依次设置不同消费者的bingingKey为{"kern.*"}、{"*.critical"}、{"kern.*", "*.critical"}
String bingingKeys[] = {"kern.*"};
for (String bindingKey : bingingKeys) {
channel.queueBind(queueName, EXCHANGE_NAME, bindingKey);
}
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
//监听消息
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body, "UTF-8");
System.out.println(" [x] Received '" + envelope.getRoutingKey() + "':'" + message + "'");
}
};
channel.basicConsume(queueName, true, consumer);
}
}