RabbitMQ之Exchange-4
RabbitMQ消息模型的核心思想是生产者不会将消息直接发送给队列。生产者通常不知道消息将会被哪些消费者接收,按照刚开始里介绍的rabbitMQ中所画的,生产者不是直接将消息发送给Queue么认识会交给Exchange,所以需要定义Exchange的消息分发模型来实现消息的分发,即发布者/订阅者模式,用这种模型来实现生产者与消费者之间的解耦。
那我们之前的例子中为什么只定义了一个队列,而没有定义Exchange,起不是违背了生产者不会将消息直接发送给队列的核心思想?我们来回顾一下之前生产者发布消息时的代码:
channel.basicPublish("", queueName, MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes());
第一个参数是空字符串,其实这个参数就是Exchange,这里定义了一个默认的Exchange。
如果用空字符串去申明一个exchange,那么系统就会使用"amq.direct"这个exchange。我们在创建一个queue的时候,默认的都会有一个和新建queue同名的routingKey绑定到这个默认的exchange上去,所以我们之前第二个参数里我们都是写的queueName。
Exchange
关于exchange,生产者只能发送消息到一个交换组件中(Exchange),exchange是一个很简单的东西,一方面它接收来自生产者的消息,另外一方面它将把来自生产者的消息放入到队列中,exchange必须知道怎么接收一个消息,而且接收的消息应该被添加到一个特定的队列?还是多个队列中?或者接收的消息被丢弃,这个规则被exchange类型所定义。
Exchange有如下几种定义类型:direct(直接)、topic(主题)、headers(标题)和fanout,可以通过命令 rabbitmqctl list_exchanges查看
以下为常用的三种类型:direct、topic、fanout.
Fanout Exchange
任何发送到Fanout Exchange的消息都会被转发到与该Exchange绑定(Binding)的所有Queue上。
1.可以理解为路由表的模式(通过exchange把消息路由到队列中)
2.这种模式不需要RouteKey
3.这种模式需要提前将Exchange与Queue进行绑定,一个Exchange可以绑定多个Queue,一个Queue可以同多个Exchange进行绑定。
4.如果接受到消息的Exchange没有与任何Queue绑定,则消息会被抛弃。
简单的关键代码示例:
生产者:
//fanout表示消息将以广播的形式分发给多个消费者
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
channel.basicPublish(EXCHANGE_NAME,"",MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
消费者:
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
String queueName = channel.queueDeclare().getQueue();
channel.queueBind(queueName, EXCHANGE_NAME, "");
完整代码:
生产者:SenderByFanout
package com.teaboy.rabitmq.client.exchanges.publish;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
/**
*
* (Fanout类型的exchange)
*
* <p>
* 修改历史: <br>
* 修改日期 修改人员 版本 修改内容<br>
* -------------------------------------------------<br>
* 2015年10月8日 上午9:44:39 user 1.0 初始化创建<br>
* </p>
*
* @author Peng.Li
* @version 1.0
* @since JDK1.7
*/
public class SenderByFanout {
private static final String EXCHANGE_NAME = "logsFanoutExchange";
public static void main(String[] argv) throws java.io.IOException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setPort(5678);
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
// fanout表示消息将以广播的形式分发给多个消费者
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
String message = "注意了:Fanout广播一条消息,注意火灾!";
// 这里的第一个参数不再是空字符串,而是ExchangeName
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
System.out.println(" send message [" + message + "]");
channel.close();
connection.close();
}
}
消费者:RecvByFanout01
package com.teaboy.rabitmq.client.exchanges.consumer;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.QueueingConsumer;
/**
*
* (fanout类型的exchange)
*
* <p>
* 修改历史: <br>
* 修改日期 修改人员 版本 修改内容<br>
* -------------------------------------------------<br>
* 2015年10月8日 下午4:10:16 user 1.0 初始化创建<br>
* </p>
*
* @author peng.li
* @version 1.0
* @since JDK1.7
*/
public class RecvByFanout01 {
private static final String EXCHANGE_NAME = "logsFanoutExchange";
public static void main(String[] argv) throws java.io.IOException, java.lang.InterruptedException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setPort(5678);
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
// fanout表示消息将以广播的形式分发给多个消费者
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
// 创建临时队列,一个非持久的、唯一的、能自动删除的队列与生成名称
String queueName = channel.queueDeclare().getQueue();
// 把临时队列绑定到EXCHANGE上
channel.queueBind(queueName, EXCHANGE_NAME, "");
QueueingConsumer consumer = new QueueingConsumer(channel);
channel.basicConsume(queueName, true, consumer);
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println(" received message[" + message + "]");
}
}
}
消费者:RecvByFanout02
package com.teaboy.rabitmq.client.exchanges.consumer;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.QueueingConsumer;
/**
*
* (fanout类型的exchange)
*
* <p>
* 修改历史: <br>
* 修改日期 修改人员 版本 修改内容<br>
* -------------------------------------------------<br>
* 2015年10月8日 下午4:10:09 user 1.0 初始化创建<br>
* </p>
*
* @author Peng.Li
* @version 1.0
* @since JDK1.7
*/
public class RecvByFanout02 {
private static final String EXCHANGE_NAME = "logsFanoutExchange";
public static void main(String[] argv) throws java.io.IOException, java.lang.InterruptedException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setPort(5678);
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
//创建临时队列,一个非持久的、唯一的、能自动删除的队列与生成名称
String queueName = channel.queueDeclare().getQueue();
// 把临时队列绑定到EXCHANGE上
channel.queueBind(queueName, EXCHANGE_NAME, "");
QueueingConsumer consumer = new QueueingConsumer(channel);
channel.basicConsume(queueName, true, consumer);
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println(" received message[" + message + "]");
}
}
}
控制台运行结果:
发送端:
接收端RecvByFanout01
接收端RecvByFanout02
RabbitMq后台管理:
从图中看出,没有Routing key 只要把队列绑定到这个exchange上就能接收到消息。
Direct Exchange:direct类型的转发器背后的路由转发算法很简单:消息会被推送至绑定键(binding key)和消息发布附带的选择键(routing key)完全匹配的队列。
日志系统广播Fanout所有的消息给所有的消费者,我们希望可以对其扩展,来允许根据日志的严重性进行过滤日志。例如:我们可能希望把致命类型的错误写入硬盘,而不把硬盘空间浪费在警告或者消息类型的日志上。之前我们使用fanout类型的转发器,但是并没有给我们带来更多的灵活性:仅仅可以愚蠢的转发。
我们将会使用direct类型的转发器进行替代。direct类型的转发器背后的路由转发算法很简单:消息会被推送至绑定键(binding key)和消息发布附带的路由键(routing key)完全匹配的队列。
图1,我们可以看到direct类型的转发器与两个队列绑定。第一个队列与绑定键orange绑定,第二个队列与转发器间有两个绑定,一个与绑定键black绑定,另一个与green绑定键绑定。
这样的话,当一个消息附带一个选择键(routing key) orange发布至转发器将会被导向到队列Q1。消息附带一个选择键(routing key)black或者green将会被导向到Q2.所有的其他的消息将会被丢弃。
多重绑定:(multiple bindings)
使用一个绑定键(binding key)绑定多个队列是完全合法的。如上图,一个附带选择键(routing key)的消息将会被转发到Q1和Q2。
图1
图2
任何发送到Direct Exchange的消息都会被转发到RouteKey中指定的Queue。
1.一般情况可以使用rabbitMQ自带的Exchange:""(该Exchange的名字为空字符串,之前称其为default Exchange)。
2.这种模式下不需要将Exchange进行任何绑定(binding)操作
3.消息传递时需要一个“RouteKey”,而且要求消息与这一路由键完全匹配,可以简单的理解为要发送到的队列名字。
4.如果vhost中不存在RouteKey中指定的队列名,则该消息会被抛弃。
简单的关键代码示例:
生产者
channel.exchangeDeclare(EXCHANGE_NAME,"direct");
channel.basicPublish(EXCHANGE_NAME,"routingKey",MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
注释:EXCHANGE_NAME可以为空字符串""。
消费者:
channel.exchangeDeclare(EXCHANGE_NAME, "direct"); String queueName = channel.queueDeclare().getQueue(); channel.queueBind(queueName, EXCHANGE_NAME, "routingKey");
完整代码:
图3
生产者:SenderDirect02
package com.teaboy.rabitmq.client.exchanges.publish; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; /** * * (类型功能说明描述) * * <p> * 修改历史: <br> * 修改日期 修改人员 版本 修改内容<br> * -------------------------------------------------<br> * 2015年10月8日 下午4:42:52 user 1.0 初始化创建<br> * </p> * * @author user * @version 1.0 * @since JDK1.7 */ public class SenderDirect02 { private static final String EXCHANGE_NAME = "directlogsExchange"; public static void main(String[] argv) throws java.io.IOException { ConnectionFactory factory = new ConnectionFactory(); factory.setHost("localhost"); factory.setPort(5678); Connection connection = factory.newConnection(); Channel channel = connection.createChannel(); // declare a non-autodelete, non-durable exchange with no extra arguments // 申明一个direct类型的exchange,rabbit服务重启的话,exchange消失 channel.exchangeDeclare(EXCHANGE_NAME, "direct"); String[] severity = { "info", "error", "waring" }; //发送消息 for (String key : severity) { String message = "消息路由key:" + key + "的消息"; channel.basicPublish(EXCHANGE_NAME, key, null, message.getBytes()); System.out.println(" [发布者] Sent '" + key + "':'" + message + "'"); } channel.close(); connection.close(); } }
消费者RecvByDirect02
package com.teaboy.rabitmq.client.exchanges.consumer; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.QueueingConsumer; /** * * (direct类型的exchange述) * * <p> * 修改历史: <br> * 修改日期 修改人员 版本 修改内容<br> * -------------------------------------------------<br> * 2015年10月8日 下午4:44:10 user 1.0 初始化创建<br> * </p> * * @author peng.Li * @version 1.0 * @since JDK1.7 */ public class RecvByDirect02 { private static final String EXCHANGE_NAME = "directlogsExchange"; public static void main(String[] argv) throws java.io.IOException, java.lang.InterruptedException { ConnectionFactory factory = new ConnectionFactory(); factory.setHost("localhost"); factory.setPort(5678); Connection connection = factory.newConnection(); Channel channel = connection.createChannel(); // declare a non-autodelete, non-durable exchange with no extra arguments channel.exchangeDeclare(EXCHANGE_NAME, "direct"); // declare a server-named exclusive, autodelete, non-durable queue. String queueName = channel.queueDeclare().getQueue(); String[] severity = { "info", "error", "waring" }; for (String key : severity) { channel.queueBind(queueName, EXCHANGE_NAME, key); } System.out.println(" [*] Waiting for messages. To exit press CTRL+C"); QueueingConsumer consumer = new QueueingConsumer(channel); channel.basicConsume(queueName, true, consumer); while (true) { QueueingConsumer.Delivery delivery = consumer.nextDelivery(); //消息体 String message = new String(delivery.getBody()); // String routingKey = delivery.getEnvelope().getRoutingKey(); System.out.println(" [消费者1] Received路由key: '" + routingKey + "',消息为:'" + message + "'"); } } }
消费者RecvByDirect022
package com.teaboy.rabitmq.client.exchanges.consumer; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.QueueingConsumer; /** * * (路由消息,接收error级别的消息) * * <p> * 修改历史: <br> * 修改日期 修改人员 版本 修改内容<br> * -------------------------------------------------<br> * 2015年10月8日 下午4:45:21 user 1.0 初始化创建<br> * </p> * * @author user * @version 1.0 * @since JDK1.7 */ public class RecvByDirect022 { private static final String EXCHANGE_NAME = "directlogsExchange"; public static void main(String[] argv) throws java.io.IOException, java.lang.InterruptedException { ConnectionFactory factory = new ConnectionFactory(); factory.setHost("localhost"); factory.setPort(5678); Connection connection = factory.newConnection(); Channel channel = connection.createChannel(); //Actively declare a non-autodelete, non-durable exchange with no extra arguments channel.exchangeDeclare(EXCHANGE_NAME, "direct"); String logLevel = "error"; //declare a server-named exclusive, autodelete, non-durable queue. //申明一个临时的队列,rabbitMq服务重启,队列会消失 String queueName = channel.queueDeclare().getQueue(); //Bind a queue to an exchange, with no extra arguments. //logLevel为routingkey="error",队列只能接收error级别的消息 channel.queueBind(queueName, EXCHANGE_NAME, logLevel); System.out.println(" [*] Waiting for messages. To exit press CTRL+C"); QueueingConsumer consumer = new QueueingConsumer(channel); channel.basicConsume(queueName, true, consumer); while (true) { QueueingConsumer.Delivery delivery = consumer.nextDelivery(); String message = new String(delivery.getBody()); //获得routingKey,接收的到底是什么消息 String routingKey = delivery.getEnvelope().getRoutingKey(); System.out.println(" [消费者2] Received路由key: '" + routingKey + "',消息为:'" + message + "'"); } } }
控制台运行结果:
生产者发送消息:
消费者RecvByDirect02接收消息:
消费者RecvByDirect022接收消息:
Topic Exchange
我们通过direct exchange进步改良了我们的日志系统。我们使用direct类型转发器,使得接收者有能力进行选择性的接收日志,,而非fanout那样,只能够无脑的转发。虽然使用direct类型改良了我们的系统,但是仍然存在一些局限性:它不能够基于多重条件进行路由选择。在我们的日志系统中,我们有可能希望不仅根据日志的级别而且想根据日志的来源进行订阅,为了在我们的系统中实现上述的需求,我们需要学习稍微复杂的主题类型的转发器(topic exchange)。
图1
图2
任何发送到Topic Exchange的消息都会被转发到所有关心RouteKey中指定主题的队列(Queue)上
1.这种模式较为复杂,简单来说,就是每个队列都有其关心的主题,所有的消息都带有一个“标题”(RouteKey),Exchange会将消息转发到所有关注主题能与RouteKey模糊匹配的队列。
2.这种模式需要RouteKey,也许要提前绑定Exchange与Queue。
3.在进行绑定时,要提供一个该队列关心的主题,如“#.log.#”表示该队列关心所有涉及log的消息。
4.“#”表示0个或若干个关键字,“*”表示一个关键字。如“log.*”能与“log.warn”匹配,无法与“log.warn.timeout”匹配;但是“log.#”能与上述两者匹配。
5.如果Exchange没有发现能够与RouteKey匹配的Queue,则会抛弃此消息。
6.当一个队列与绑定键#绑定,将会收到所有的消息,类似fanout类型转发器,当绑定键中不包含任何#与*时,类似direct类型转发器。
简单的关键代码示例:
生产者:
channel.exchangeDeclare(EXCHANGE_NAME,"topic"); String[] keys ={"USA.weather.1","China.weather.1","USA.people.1","China.people.1"}; for(String key:keys){ String msg = key+":消息"; channel.basicPublish(EXCHANGE_NAME,key,null,msg.getBytes()) }
消费者:
channel.exchangeDeclare(EXCHANGE_NAME, "topic"); String queue = channel.queueDeclare().getQueue(); String key ="*.weather.*"; channel.queueBind(queue, EXCHANGE_NAME, key);
完整代码:
生产者:SenderTopic
package com.teaboy.rabitmq.client.exchanges.publish; import java.io.IOException; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; /** * * (topic类型的exchangge) * * <p> * 修改历史: <br> * 修改日期 修改人员 版本 修改内容<br> * -------------------------------------------------<br> * 2015年10月8日 下午3:23:46 user 1.0 初始化创建<br> * </p> * * @author Peng.Li * @version 1.0 * @since JDK1.7 */ public class SenderTopic { private static final String EXCHANGE_NAME="topicExchange"; public static void main(String[] args) throws IOException { ConnectionFactory factory = new ConnectionFactory(); factory.setHost("localhost"); factory.setPort(5678); Connection con = factory.newConnection(); Channel channel = con.createChannel(); //申明一个topic类型的EXCHANGE channel.exchangeDeclare(EXCHANGE_NAME,"topic"); String[] keys ={"USA.weather.1","China.weather.1","USA.people.1","China.people.1"}; for(String key:keys){ String msg = key+":消息"; channel.basicPublish(EXCHANGE_NAME, key, null,msg.getBytes()); System.out.println("send"+"["+msg+"]"); } channel.close(); con.close(); } }
消费者RecvByTopic01:
package com.teaboy.rabitmq.client.exchanges.consumer; import java.io.IOException; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.ConsumerCancelledException; import com.rabbitmq.client.QueueingConsumer; import com.rabbitmq.client.ShutdownSignalException; import com.rabbitmq.client.QueueingConsumer.Delivery; /** * * (类型功能说明描述) * * <p> * 修改历史: <br> * 修改日期 修改人员 版本 修改内容<br> * -------------------------------------------------<br> * 2015年10月8日 下午3:41:54 user 1.0 初始化创建<br> * </p> * * @author Peng.Li * @version 1.0 * @since JDK1.7 */ public class RecvByTopic01 { private static final String EXCHANGE_NAME="topicExchange"; public static void main(String[] args) throws IOException, ShutdownSignalException, ConsumerCancelledException, InterruptedException { ConnectionFactory factory = new ConnectionFactory(); factory.setHost("localhost"); factory.setPort(5678); Connection con = factory.newConnection(); Channel channel = con.createChannel(); //定义一个类型为topic的exchange channel.exchangeDeclare(EXCHANGE_NAME, "topic"); String queue = channel.queueDeclare().getQueue(); //接收routingKey 满足*.weather.*类型的消息 String[] keys ={"*.weather.*"}; for(String key:keys){ channel.queueBind(queue, EXCHANGE_NAME, key); } QueueingConsumer consumer = new QueueingConsumer(channel); channel.basicConsume(queue, true, consumer); //循环获取消息 while(true){ //获取消息,如果没有消息,这一步将会一直阻塞 Delivery delivery = consumer.nextDelivery(); String message = new String(delivery.getBody()); String routingKey = delivery.getEnvelope().getRoutingKey(); System.out.println(" [消费者1] Received路由key: '*.weather.*',消息为:'" + message + "'"); } } }
消费者RecvByTopic02:
package com.teaboy.rabitmq.client.exchanges.consumer; import java.io.IOException; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.ConsumerCancelledException; import com.rabbitmq.client.QueueingConsumer; import com.rabbitmq.client.ShutdownSignalException; import com.rabbitmq.client.QueueingConsumer.Delivery; /** * * (topic类型的exchange) * * <p> * 修改历史: <br> * 修改日期 修改人员 版本 修改内容<br> * -------------------------------------------------<br> * 2015年10月8日 下午3:41:36 user 1.0 初始化创建<br> * </p> * * @author Peng.Li * @version 1.0 * @since JDK1.7 */ public class RecvByTopic02 { private static final String EXCHANGE_NAME="topicExchange"; public static void main(String[] args) throws IOException, ShutdownSignalException, ConsumerCancelledException, InterruptedException { ConnectionFactory factory = new ConnectionFactory(); factory.setHost("localhost"); factory.setPort(5678); Connection con = factory.newConnection(); Channel channel = con.createChannel(); //定义一个类型为topic的exchange channel.exchangeDeclare(EXCHANGE_NAME, "topic"); String queue = channel.queueDeclare().getQueue(); //接收#.1类型的消息,#表示0或者若干个关键字 String[] keys ={"#.1"}; for(String key:keys){ channel.queueBind(queue, EXCHANGE_NAME, key); } QueueingConsumer consumer = new QueueingConsumer(channel); channel.basicConsume(queue, true, consumer); //循环获取消息 while(true){ //获取消息,如果没有消息,这一步将会一直阻塞 Delivery delivery = consumer.nextDelivery(); String message = new String(delivery.getBody()); String routingKey = delivery.getEnvelope().getRoutingKey(); System.out.println(" [消费者2] Received路由key: '#.1',消息为:'" + message + "'"); } } }
控制台输出结果:
发送消息:
消费者RecvByTopic01:
消费者RecvByTopic02:
RabbitMq后台管理系统:
可以看出RecvByTopic01接收到了消息'USA.weather.1:消息'和'China.weather.1:消息',不能接收到'USA.people.1:消息'和'China.people.1:消息',由于是top类型的exchange,配置的bind策略是Routing key = *.weather.*。所以出现以上结果,而RecvByTopic02由于配置的策略是#.1,以.1结尾的消息都能被接收。