前面改良了我们的日志系统。我们使用direct类型转发器,使得接收者有能力进行选择性的接收日志,,而非fanout那样,只能够无脑的转发。
虽然使用direct类型改良了我们的系统,但是仍然存在一些局限性:它不能够基于多重条件进行路由选择。
在我们的日志系统中,我们有可能希望不仅根据日志的级别而且想根据日志的来源进行订阅。这个概念类似unix工具:syslog,它转发日志基于严重性(info/warning/crit…)和设备(auth/cron/kern…)
这样可能给我们更多的灵活性:我们可能只想订阅来自’cron’的致命错误日志,而不是来自’kern’的。
为了在我们的系统中实现上述的需求,我们需要学习稍微复杂的主题类型的转发器(topic exchange)。
1、 主题转发(Topic Exchange)
发往 主题类型的转发器 的消息 不能随意的设置选择键(routing_key),必须是由点隔开的一系列的标识符组成。标识符可以是任何东西,但是一般都与消息的某些特性相关。一些合法的选择键的例子:"stock.usd.nyse", "nyse.vmw","quick.orange.rabbit".你可以定义任何数量的标识符,上限为255个字节。
绑定键(转发器与队列绑定)和选择键(消息和转发器)的形式一样。主题类型的转发器背后的逻辑和直接类型的转发器很类似:一个附带特殊的选择键将会被转发到绑定键与之匹配的队列中。需要注意的是:关于绑定键有两种特殊的情况。
*可以匹配一个标识符。
#可以匹配0个或多个标识符。
Producer发送消息时需要设置选择键(routing_key),例如选择键(routing_key)包含三个单词和两个点号。第一个key是描述了celerity(灵巧,敏捷),第二个是colour(色彩),第三个是species(物种):"<celerity>.<colour>.<species>"。
在这里我们创建了两个绑定: Q1 的binding key 是"*.orange.*"; Q2 是 "*.*.rabbit" 和 "lazy.#":
Q1 感兴趣所有orange颜色的动物
Q2 感兴趣所有的rabbits和所有的lazy的
比如routing_key是 "quick.orange.rabbit"将会发送到Q1和Q2中。消息"lazy.orange.elephant" 也会发送到Q1和Q2。但是"quick.orange.fox" 会发送到Q1;"lazy.brown.fox"会发送到Q2。"lazy.pink.rabbit" 也会发送到Q2,但是尽管两个routing_key都匹配,它也只是发送一次。"quick.brown.fox" 会被丢弃。
由于“#”可以代表多个标识符,所以也可能是四个或者更多的单词。
2.Topic exchange和其他exchange
由于有"*" (star) and "#" (hash), Topic exchange(主题类型的转发器)非常强大并且可以转化为其他的exchange:
如果binding_key(绑定键) 是 "#" - 它会接收所有的Message,不管routing_key(选择键)是什么,就像是fanout exchange。
如果 "*" (star) and "#" (hash) 没有被使用,那么topic exchange就变成了direct exchange.
package com.bj.rabbitmq.study.four;
import java.io.IOException;
import java.util.UUID;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
public class TopicSend {
private static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] args) throws IOException {
//创建连接 连接到rabbitmq消息队列
ConnectionFactory cf = new ConnectionFactory();
//设置RabbitMQ所在主机ip或者主机名
cf.setHost("localhost");
//创建一个连接
Connection c = cf.newConnection();
//创建一个频道
Channel ch = c.createChannel();
ch.exchangeDeclare(EXCHANGE_NAME, "topic");
String[] routing_keys = new String[] { "kernal.info", "cron.warning",
"auth.info", "kernel.critical" };
for (String routing_key : routing_keys){
String msg = UUID.randomUUID().toString();
ch.basicPublish(EXCHANGE_NAME, routing_key, null, msg.getBytes());
System.out.println(" [x] Sent routingKey = "+routing_key+" ,msg = " + msg + ".");
}
ch.close();
c.close();
}
}
该例发送四条消息,分别设置了不同的选择键(routing_key)。
接收端1 接收和Kernel相关的日志消息。
package com.bj.rabbitmq.study.four;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.QueueingConsumer;
public class TopoicRecvKernel {
private static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] argv) throws java.io.IOException,
java.lang.InterruptedException
{
//打开连接和创建频道,与发送端一样
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();
//接收所有与kernel相关的消息
channel.queueBind(queueName, EXCHANGE_NAME, "kernel.*");
System.out.println(" [Waiting] 等待消息。。。");
//创建队列消费者
QueueingConsumer consumer = new QueueingConsumer(channel);
//指定消费队列
channel.basicConsume(queueName, true, consumer);
while (true)
{
//nextDelivery是一个阻塞方法(内部实现其实是阻塞队列的take方法)
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
String routingKey = delivery.getEnvelope().getRoutingKey();
System.out.println(" [Received]:routingKey = " + routingKey +",msg=" + message);
}
}
}
接收端2 只接收致命错误的日志消息。
package com.bj.rabbitmq.study.four;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.QueueingConsumer;
public class TopoicRecvCritical {
private static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] argv) throws java.io.IOException,
java.lang.InterruptedException
{
//打开连接和创建频道、通道,与发送端一样
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();
//接收所有与critical相关的消息
channel.queueBind(queueName, EXCHANGE_NAME, "*.critical");
System.out.println(" [Waiting] 等待消息。。。");
//创建队列消费者
QueueingConsumer consumer = new QueueingConsumer(channel);
//指定消费队列
channel.basicConsume(queueName, true, consumer);
while (true)
{
//nextDelivery是一个阻塞方法(内部实现其实是阻塞队列的take方法)
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
String routingKey = delivery.getEnvelope().getRoutingKey();
System.out.println(" [Received]:routingKey = " + routingKey +",msg=" + message);
}
}
}
分别启动2个接收端,然后启动发送端,日志记录如下
发送端-----------------------------------------------------
[x] Sent routingKey = kernal.info ,msg = 8a1c31fa-a249-4760-ad76-0b6698cb29cc.
[x] Sent routingKey = cron.warning ,msg = b48d0767-fdb5-4b89-b919-d7d4fe7f088d.
[x] Sent routingKey = auth.info ,msg = 63bdcf76-adef-4210-8d47-825d53715900.
[x] Sent routingKey = kernel.critical ,msg = efb0b346-64a0-470c-9ad5-93bb836cbec0.
接收端1------------------------------------------------------
[Waiting] 等待消息。。。
[Received]:routingKey = kernel.critical,msg=efb0b346-64a0-470c-9ad5-93bb836cbec0
接收端2------------------------------------------------------
[Waiting] 等待消息。。。
[Received]:routingKey = kernel.critical,msg=efb0b346-64a0-470c-9ad5-93bb836cbec0
通过使用topic类型的转发器,成功实现了多重条件选择的订阅。
参考: