1、主题
在前面的例子中,我们对日志系统进行了改进。使用了direct交换机代替了fanout交换机,从只能盲目的广播消息改进为有可能选择性的接收日志。
尽管直接交换机能够改善我们的日志系统,但是它也有它的限制——没办法基于多个标准执行路由操作。
在我们的日志系统中,我们不只希望订阅基于日志级别,同时还希望订阅基于日志来源。其中unix工具syslog是同时基于日志的级别(info/warn/error)和设备-facility (auth/cron/kern...)来路由日志的。
如果这样的话,将会给予我们非常大的灵活性,我们既可以监听来源于“cron”的严重程度为“critical errors”的日志,也可以监听来源于“kern”的所有日志。
为了实现这个目的,接下来我们学习如何使用另一种更复杂的交换机 —— 主题交换机。
2、主题交换机
发送到主题交换机(topic exchange)的消息不可以携带随意什么样子的路由键(routing_key),它的路由键必须是一个由.
分隔开的词语列表。这些单词随便是什么都可以,但是最好是跟携带它们的消息有关系的词汇。以下是几个推荐的例子:"stock.usd.nyse", "nyse.vmw", "quick.orange.rabbit"。词语的个数可以随意,但是不要超过255字节。
绑定键也必须拥有同样的格式。主题交换机背后的逻辑跟直连交换机很相似 —— 一个携带着特定路由键的消息会被主题交换机投递给绑定键与之想匹配的队列。但是它的绑定键和路由键有两个特殊应用方式:
*
(星号) 用来表示一个单词.
#
(井号) 用来表示任意数量(零个或多个)单词
下边用图说明:
这个例子里,我们发送的所有消息都是用来描述小动物的。发送的消息所携带的路由键是由三个单词所组成的,这三个单词被两个.
分割开。路由键里的第一个单词描述的是动物的手脚的利索程度,第二个单词是动物的颜色,第三个是动物的种类。所以它看起来是这样的: <celerity>.<colour>.<species>
。
我们创建了三个绑定:Q1的绑定键为 *.orange.*
,Q2的绑定键为 *.*.rabbit
和 lazy.#
。
这三个绑定键被可以总结为:
Q1 对所有的桔黄色动物都感兴趣。
Q2 则是对所有的兔子和所有懒惰的动物感兴趣。
一个携带有 quick.orange.rabbit
的消息将会被分别投递给这两个队列。携带着 lazy.orange.elephant
的消息同样也会给两个队列都投递过去。另一方面携带有 quick.orange.fox
的消息会投递给第一个队列,携带有 lazy.brown.fox
的消息会投递给第二个队列。携带有 lazy.pink.rabbit
的消息只会被投递给第二个队列一次,即使它同时匹配第二个队列的两个绑定。携带着 quick.brown.fox
的消息不会投递给任何一个队列。
如果我们违反约定,发送了一个携带有一个单词或者四个单词("orange"
or "quick.orange.male.rabbit"
)的消息时,发送的消息不会投递给任何一个队列,而且会丢失掉。
但是另一方面,即使 "lazy.orange.male.rabbit"
有四个单词,他还是会匹配最后一个绑定,并且被投递到第二个队列中。
注意:
主题交换机是很强大的,它可以表现出跟其他交换机类似的行为
当一个队列的绑定键为 "#"(井号) 的时候,这个队列将会无视消息的路由键,接收所有的消息。
当 *
(星号) 和 #
(井号) 这两个特殊字符都未在绑定键中出现的时候,此时主题交换机就拥有的直连交换机的行为。
3、组合在一起
接下来我们会将主题交换机应用到我们的日志系统中。在开始工作前,我们假设日志的路由键由两个单词组成,路由键看起来是这样的:<facility>.<severity>。
代码跟上一篇教程差不多。只是参数和交换机类型不一样。
EmitLogTopic.java代码如下:
1 package rabbitmq.topic; 2 3 import java.io.IOException; 4 import java.util.concurrent.TimeoutException; 5 6 import com.rabbitmq.client.Channel; 7 import com.rabbitmq.client.Connection; 8 9 import rabbitmq.utils.ConnectionUtils; 10 11 /* 12 * 发送错误日志生产者 13 */ 14 public class EmitLogTopic { 15 private static final String EXCHANGE_NAME = "topic_logs"; 16 public static void main(String[] args) throws IOException, TimeoutException { 17 //获取连接 18 Connection connection = ConnectionUtils.getConnection(); 19 //创建管道 20 Channel channel = connection.createChannel(); 21 //创建直接交换器 22 channel.exchangeDeclare(EXCHANGE_NAME, "topic"); 23 //获取日志级别 24 String severity = getSeverity(args); 25 //获取消息 26 String message = getMessage(args); 27 //发送消息 28 channel.basicPublish(EXCHANGE_NAME, severity, null, message.getBytes()); 29 System.out.println(" [x] Sent '" + severity + "':'" + message + "'"); 30 channel.close(); 31 connection.close(); 32 } 33 }
ReceiveLogsTopic.java代码如下:
1 package rabbitmq.topic; 2 3 import java.io.IOException; 4 import java.util.concurrent.TimeoutException; 5 6 import com.rabbitmq.client.AMQP; 7 import com.rabbitmq.client.Channel; 8 import com.rabbitmq.client.Connection; 9 import com.rabbitmq.client.Consumer; 10 import com.rabbitmq.client.DefaultConsumer; 11 import com.rabbitmq.client.Envelope; 12 13 import rabbitmq.utils.ConnectionUtils; 14 15 public class ReceiveLogsTopic { 16 private static final String EXCHANGE_NAME = "topic_logs"; 17 18 public static void main(String[] args) throws IOException, TimeoutException { 19 // 获取连接 20 Connection connection = ConnectionUtils.getConnection(); 21 // 创建管道 22 Channel channel = connection.createChannel(); 23 // 创建直接交换器 24 channel.exchangeDeclare(EXCHANGE_NAME, "topic"); 25 String queueName = channel.queueDeclare().getQueue(); 26 for (String severity : args) { 27 channel.queueBind(queueName, EXCHANGE_NAME, severity); 28 } 29 Consumer consumer = new DefaultConsumer(channel) { 30 @Override 31 public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, 32 byte[] body) throws IOException { 33 String message = new String(body, "UTF-8"); 34 System.out.println(" [x] Received '" + envelope.getRoutingKey() + "':'" + message + "'"); 35 } 36 }; 37 channel.basicConsume(queueName, true, consumer); 38 } 39 }
ReceiveLogsTopic.java分别启动四次,参数分别为"#"、"kern.*"、"*.critical"、"kern.*" "*.critical"。
启动EmitLogTopic.java,参数为"kern.critical" "A critical kernel error",其中routing_key=“kern.critical”,那么绑定键为"#"、"kern.*"、"*.critical"都会收到日志信息