原文链接:www.rabbitmq.com/tutorials/t…
在上一章中我们对日志系统进行了改进,将fanout类型的广播交换机替换为direct类型的键绑定交换机,这样就可以根据日志级别来选择性的接收日志消息。
尽管使用direct交换机已经有了很大程度的改进,但还有一个问题就是匹配规则太单一无法满足复杂的匹配规则。
有时候我们不光要根据日志级别来订阅消息,还要根据日志的来源来订阅。你或许从unix的syslog工具中了解过相关概念,它是根据日志的级别(info/warn/crit…)以及系统模块(auth/cron/kern...)来进行路由的。
这就让系统变得更加灵活,我们可能只需要监听cron发出的error级别的日志,以及kern发出的所有日志。
为了在日志系统中实现这种需求,我们需要了解更复杂的topic交换机。
Topic交换机
发送到topic交换机的消息的路由键必须满足一定的格式:用点分隔的单词列表。这些词可以使任何东西,不过一般情况下都是一些描述消息特征的词,比如:"stock.usd.nyse", "nyse.vmw", “quick.orange.rabbit”。路由键中词的数量随你定,只要最终的RoutingKey不超过255byte的限制就行。
绑定建的必须和路由键的格式保持一致。其实topic交换机和direct交换机的机制类似,都是将包含特定路由键的消息分发给能够匹配相应绑定建的队列。不过topic对应的绑定建有注意两点:
- *可以用来替代任意一个词。
- #可以用来替代多个词。
其实很容易理解:
在上图的示例中,我们发送的消息都是用来描述动物的。这些消息的路由键都是三个词组成(两个分隔点),第一个词描述速度,第二个词描述颜色,最后一个词描述物种:"..”。
创建的两个绑定关系: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队列中。
Topic交换机备注:
topic交换机功能强大,能够像其他类型的交换机一样工作。
当队列绑定的建是“#”时,无论消息的路由键是什么,这个队列就会接收所有的消息,就像是在使用fanout交换机。
当绑定建中没有*和#时,topic交换机就会像direct交换机一样工作。
代码整合
这里我们要创建一个用topic交换机支撑的日志系统,我们假定所有消息的路由键都是两个词描述的,格式为".”。
这块的代码和上一章中的代码类型。
EmitLogTopic.java代码:
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
public class EmitLogTopic {
private static final String EXCHANGE_NAME = "topic_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, "topic");
String routingKey = getRouting(argv);
String message = getMessage(argv);
channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + routingKey + "':'" + message + "'");
}
}
//..
}
ReceiveLogsTopic.java代码:
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;
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, "topic");
String queueName = channel.queueDeclare().getQueue();
if (argv.length < 1) {
System.err.println("Usage: ReceiveLogsTopic [binding_key]...");
System.exit(1);
}
for (String bindingKey : argv) {
channel.queueBind(queueName, EXCHANGE_NAME, bindingKey);
}
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 '" +
delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
}
}
编译运行该样例时所需的依赖与第一章一样,Windows中引用环境变量使用%CP%:
编译:
javac -cp $CP ReceiveLogsTopic.java EmitLogTopic.java
接收所有消息:
java -cp $CP ReceiveLogsTopic "#"
接收kern模块的所有消息:
java -cp $CP ReceiveLogsTopic "kern.*"
只接受critical类型的日志消息:
java -cp $CP ReceiveLogsTopic "*.critical"
多重绑定:
java -cp $CP ReceiveLogsTopic "kern.*" "*.critical"
发送路由键为“kern.critical”的日志消息:
java -cp $CP EmitLogTopic "kern.critical" "A critical kernel error"
尽情折腾吧,所有的路由键和绑定建你可以随便去设置,只要符合RabbitMQ的规则就可以了。
本章完整代码可以查看EmitLogTopic、ReceiveLogTopic
下一章中我们来了解如何用RabbitMQ来执行RPC调用。