本节将会介绍RabbitMQ中的topic(主题)交换机,通过在上一篇博客的基础上,利用topic形式的exchange对程序进行改进,将会变得更加灵活。
发送到topic交换机的消息不能具有任意的 routing_key —— 它必须是由点分隔的单词列表。单词可以是任何内容,但通常它们指定与消息相关的一些功能。一些有效的路由键示例:“ stock.usd.nyse ”,“ nyse.vmw ”,“ quick.orange.rabbit ”。其中可以包含任意数量的单词,最多可达255个字节。
binding key也必须采用相同的形式。topic交换机背后的逻辑 类似于direct交换机——使用特定 routing key 发送的消息将被传递到与匹配binding key绑定的所有队列。但是binding key有两个重要的特殊情况:
在一个例子中解释这个是最容易的:
我们准备发送关于动物的消息。消息会附加一个选择键包含3个标识符(两个点隔开)。第一个标识符描述动物的速度,第二个标识符描述动物的颜色,第三个标识符描述动物的物种:
我们创建3个绑定键:Q1与*.orange.*绑定Q2与*.*.rabbit和lazy.#绑定。可以概括为:
Q1对所有的橙色动物感兴趣。
Q2想要知道关于兔子的一切以及关于懒洋洋的动物的一切。
一个附带quick.orange.rabbit的选择键的消息将会被转发到两个队列。附带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。
注意:主题类型的转发器非常强大,可以实现其他类型的转发器。
当一个队列与绑定键#绑定,将会收到所有的消息,类似fanout类型转发器。
当绑定键中不包含任何#与*时,类似direct类型转发器。
生产者的routing key会从数组{ "order.info", "order.warn.middle", "order.error.high" ,"user.info" }中随机选择一个,消费者1的binding key为"*.info",则如果发送的消息的routing key 以一个单词开头以info结尾(中间以"."分隔)会路由到消费者1;消费者2的binding key为"order.#",则如果发送的消息的routing key 如果以order开头都会路由到消费者2。
生产者代码如下:项目GitHub地址 https://github.com/RookieMember/RabbitMQ-Learning.git。
package cn.wkp.rabbitmq.newest.exchange.topic;
import java.util.Random;
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 = "topic_exchange";
private static String[] logLevels = { "order.info", "order.warn.middle", "order.error.high" ,"user.info" };
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
// 从连接中创建通道
Channel channel = connection.createChannel();
// 声明交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
Random random = new Random();
// 消息内容
for (int i = 0; i < 10; i++) {
// 随机得到路由键
String routingKey = logLevels[random.nextInt(logLevels.length)];
String message = "日志序号i:" + i + ",级别:" + routingKey;
channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes());
System.out.println("Sent message:" + message);
}
// 关闭通道和连接
channel.close();
connection.close();
}
}
消费者1代码如下:
package cn.wkp.rabbitmq.newest.exchange.topic;
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 = "topic_exchange";
private final static String QUEUE_NAME = "topic_queue1";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
final Channel channel = connection.createChannel();
// 声明交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定队列到交换机 *可以代替一个单词
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "*.info");
// 指该消费者在接收到队列里的消息但没有返回确认结果之前,它不会将新的消息分发给它。
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.topic;
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 = "topic_exchange";
private final static String QUEUE_NAME = "topic_queue2";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
final Channel channel = connection.createChannel();
// 声明交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定队列到交换机 #可以代表0个或多个单词
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "order.#");
// 指该消费者在接收到队列里的消息但没有返回确认结果之前,它不会将新的消息分发给它。
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);
}
}
运行结果如下(因为routing key是随机的,所以每次运行结果会不一样):
Sent message:日志序号i:0,级别:order.error.high
Sent message:日志序号i:1,级别:user.info
Sent message:日志序号i:2,级别:user.info
Sent message:日志序号i:3,级别:order.error.high
Sent message:日志序号i:4,级别:order.info
Sent message:日志序号i:5,级别:order.warn.middle
Sent message:日志序号i:6,级别:order.warn.middle
Sent message:日志序号i:7,级别:order.info
Sent message:日志序号i:8,级别:order.warn.middle
Sent message:日志序号i:9,级别:order.info
消费者1收到消息:日志序号i:1,级别:user.info
消费者1收到消息:日志序号i:2,级别:user.info
消费者1收到消息:日志序号i:4,级别:order.info
消费者1收到消息:日志序号i:7,级别:order.info
消费者1收到消息:日志序号i:9,级别:order.info
消费者2收到消息:日志序号i:0,级别:order.error.high
消费者2收到消息:日志序号i:3,级别:order.error.high
消费者2收到消息:日志序号i:4,级别:order.info
消费者2收到消息:日志序号i:5,级别:order.warn.middle
消费者2收到消息:日志序号i:6,级别:order.warn.middle
消费者2收到消息:日志序号i:7,级别:order.info
消费者2收到消息:日志序号i:8,级别:order.warn.middle
消费者2收到消息:日志序号i:9,级别:order.info