Topics
在之前的教程中,我们改进了日志记录系统。我们没有使用fanout交换机,而是使用了direct交换机,并获得了选择性接收日志的可能性。
尽管使用direct交换改进了我们的系统,但它仍然有局限性 - 它不能根据多个标准进行路由。
在我们的日志系统中,我们可能不仅需要根据严重性来订阅日志,还要根据发布日志的来源进行订阅。您可能从syslog unix工具知道这个概念,该 工具根据严重性(info/warn/crit...)和设备(auth / cron / kern ...)来路由日志。
这将给我们带来很大的灵活性——我们可能想要听取来自“cron”的关键错误,但也要听“kern”的所有日志。
要在我们的日志系统中实现这一点,我们需要了解一个更复杂的topic交换机。
Topic exchange(主题交换)
发送到topic exchange的消息不能给一个随意的routing_key—它必须是一个由点分隔开的单词列表。单词可以是任意的,但通常它们会指定一些与消息相关的特性。一些有效的routing key示例: "stock.usd.nyse", "nyse.vmw", "quick.orange.rabbit"。在routing key中可以有任意多个单词,最多可以达到255个字节。
binding key也必须以相同的形式。主题交换背后的逻辑类似于一个直接的逻辑——发送带有特定路由键的消息将被传递到绑定具有匹配绑定键的所有队列。然而,对binding key来说需要两个重要的特定场景:
*(star)可以代替一个词。
#(hash)可以替代零个或多个单词。
最容易解释的例子:
在这个例子中,我们将发送所有描述动物的信息。消息将发送一个由三个单词组成的routing key(两个点)。routing中的第一个词将描述速度、第二个颜色和第三个物种:“"<speed>.<colour>.<species>"”。
这些绑定可以概括为:
Q1对所有的橙色动物都感兴趣。
Q2对兔子和所有懒惰的动物的一切都感兴趣
一个带有"quick.orange.rabbit" 路由键的消息会被推送到两个队列。消息“lazy.orange.elephant“也会被推送到两个队列。另一方面,“quick.orange.fox“只会推送到第一个队列,“lazy.brown.fox”只会推送到第二个队列。“lazy.pink.rabbit”只会被推送到第一个队列一次,虽然它匹配了两个绑定关系。 "quick.brown.fox" 没有匹配任何绑定,所有会被丢弃。
如果我们违反约定,用一个或四个字比如"orange" 或 "quick.orange.male.rabbit"发送信息会发生什么呢?这些消息不匹配任何绑定,将丢失。
另一方面对于"lazy.orange.male.rabbit", 虽然它有4个单词,但它将匹配最后一个绑定,并将被传递到第二个队列。
总结:
主题交换机很强大,可以表现得像其他交换机一样。
当一个队列与“#”(hash)绑定键绑定时,它将接收所有消息,而不考虑在路由键--像fanout交换机一样。
当特殊字符“*”(star)和“#”(hash)不在binding key中使用时,topic exchange就会像direct exchange一样工作。
整合
我们将在日志系统中使用主题交换机。我们假设,日志的路由键将有两个词: "<facility>.<severity>"
ReceiveLogsTopic.java
package com.rabbitmq.tutorials.topic; import com.rabbitmq.client.*; import com.sun.deploy.util.StringUtils; import java.io.IOException; import java.util.Arrays; public class ReceiveLogsTopic { private static final String EXCHANGE_NAME = "topic_logs"; //private static final String[] ROUTE_KEY = {"#"}; //private static final String[] ROUTE_KEY = {"kern.*"}; //private static final String[] ROUTE_KEY = {"*.critical"}; private static final String[] ROUTE_KEY = {"kern.*","*.critical"}; public static void main(String[] argv) throws Exception { ConnectionFactory factory = new ConnectionFactory(); factory.setHost("192.168.0.103"); Connection connection = factory.newConnection(); Channel channel = connection.createChannel(); channel.exchangeDeclare(EXCHANGE_NAME, "topic");//声明交换器 String queueName = channel.queueDeclare().getQueue(); //随机生成queueName。queueName包含一个随机队列名称。例如,它可能看起来像amq.gen-JzTY20BRgKO-HjmUJj0wLg。 for (String routeKey: ROUTE_KEY) { channel.queueBind(queueName, EXCHANGE_NAME, routeKey); //将交换器和队列绑定 } System.out.println(" [*] Waiting for [" + StringUtils.join(Arrays.asList(ROUTE_KEY)," ") + "] 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); } }
EmitLogTopic.java
package com.rabbitmq.tutorials.topic; 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("192.168.0.103"); Connection connection = factory.newConnection(); Channel channel = connection.createChannel(); channel.exchangeDeclare(EXCHANGE_NAME, "topic"); int messageCount = 1; String routingKey = ""; while(messageCount<=10) { String message = "Message "+ messageCount; if(0 == messageCount%4) { routingKey = "error"; message = "error " + message; } else if(0 == messageCount%3) { routingKey = "kern.critical"; message = "kern.critical " + message; } else if(0 == messageCount%2) { routingKey = "auth.critical"; message = "auth.critical " + message; } else { routingKey = "auth.info"; message = "auth.info " + message; } channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes()); System.out.println("[x] Sent'" + message + "'"); messageCount +=1; } channel.close(); connection.close(); } //... }
执行方案:
- 切换ROUTE_KEY启动4个消费者ReceiveLogsTopic
- 启动EmitLogTopic