今天来学习RabbitMQ的消息分发策略。
在RabbitMQ札记-RabbitMQ入门一文中,我们曾学习过RabbitMQ的概念模型,其中就介绍过消息分发策略。
Exchange表示交换器,用来接收生产者发送的消息并将这些消息路由给队列。从图中可以看出,Procuder发布的Message进入了Exchange。Exchange通过Routing key与Queue绑定在一起。通过Routing key, RabbitMQ可以得知应该把这个Message放到哪个Queue里。Exchange分发消息时根据类型的不同分发策略有区别,目前共五种类型:fanout、direct、topic、headers、x-delayed-message。
fanout。每个发到 fanout 类型交换器的消息都会分到所有绑定的队列上去。
direct。如果Routing key匹配, 那么Message就会被传递到相应的Queue中。比如key可以传递到Routing key为key的Queue。
topic。对Routing key进行模式匹配,比如key*可以传递到Routing key为key1、key2、key3的Queue。
为了理解消息分发策略,我们将建立一个简单的日志系统。它将包含两种程序,一个Producer和两个Consumer。Producer将发送日志消息,Consumer将接收并打印消息。
在前面的文章中,每个Message都是deliver给一个Consumer。今天我们学习如何将一个Message传递给多个Consumer,这种模式被称为“发布/订阅”模式。
分发策略中的fanout就是广播模式,每个发到 fanout 类型Exchange的消息都会发到所有绑定的队列上去。让我们创建一个这种类型的Exchange,并将其命名为logs:
channel.exchangeDeclare(“logs”,“fanout”);
现在,我们可以发布Message到名为logs的Exchange了:
channel.basicPublish("logs","",null,message.getBytes());
截至现在,我们用的queue都是有名字的,如testQueue。但是对于我们将要构建的日志系统,并不需要有名字的queue。当我们不给queueDeclare()提供参数时,将用一个自动生成的名称创建一个非持久的,独占的自动删除队列:
String queueName = channel.queueDeclare().getQueue();
此时queueName是一个随机的队列名称,它可能看起来像amq.gen-JzTY20BRgKO-HjmUJj0wLg。
我们已经学习了如何创建fanout 类型的Exchange和临时队列。现在我们需要将两者绑定起来。
channel.queueBind(queueName,"logs","");
Producer.java
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Producer {
private final static String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws IOException, TimeoutException {
// 创建一个到服务器的连接
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername("yst");
factory.setPassword("yst");
factory.setHost("192.168.17.64");
Connection conn = factory.newConnection();
Channel channel = conn.createChannel();
//发布消息到我们的exchange,而不是队列
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
// 发送的消息
String message = "Hello World.";
// 发消息
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
System.out.println("Sent:" + message);
// 关闭渠道和连接;
channel.close();
conn.close();
}
}
Consumer.java
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Consumer {
private final static String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws IOException, TimeoutException {
// 创建一个到服务器的连接
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername("yst");
factory.setPassword("yst");
factory.setHost("192.168.17.64");
Connection conn = factory.newConnection();
Channel channel = conn.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
//获取channel绑定的队列的名字
String queueName = channel.queueDeclare().getQueue();
//将queue与exchange绑定
channel.queueBind(queueName, EXCHANGE_NAME, "");
System.out.println("Waiting for messages.");
// 创建队列消费者
final DefaultConsumer 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("Received:" + message);
}
};
channel.basicConsume(queueName, true, consumer);
}
}
启动Consumer的两个实例C1和C2,再运行Producer发送消息,查看结果
C1:
Waiting for messages.
Received:Hello World.
C2:
Waiting for messages.
Received:Hello World.
从结果中可以看出,两个Consumer都收到了消息。从管理平台中可以看到,logs交换器确实绑定了两个queue。
在上文中,我们创建了一个简单的日志系统,它可以将消息广播给多个Consumer。在本文中,我们将通过direct模式为其添加一个功能。Consumer将只能订阅一部分消息。
direct意为如果Routing key匹配, 那么Message就会被传递到相应的Queue中。比如Routing key为key的Exchange可以分发消息到Routing key为key的Queue。
从图中我们可以看到两个队列绑定的直接交换。第一个队列用绑定键key1绑定,第二个队列有两个绑定,一个绑定键为key2,另一个为key3。通过路由键key1发布到Exchange的消息将被路由到队列Queue1。通过路由键key2或key3发布到Exchange的消息将被路由到队列Queue2。其他消息将被丢弃。
绑定是exchange和queue之间的关系。在前面的例子中,我们已经创建了绑定,queueName为队列的名字,“logs”为Exchange的名字。
channel.queueBind(queueName,"logs","");
绑定可以采用额外的routingKey参数。
channel.queueBind(queueName,"logs",routingKey);
对于fanout的exchange来说,这个参数是被忽略的。
绑定多个队列是完全合法的。
在这种情况下,直接交换就像广播一样,将消息广播到所有的匹配队列。
Producer.java
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Producer {
private final static String EXCHANGE_NAME = "direct_logs";
private final static String ROUTING_KEY = "key1";
public static void main(String[] args) throws IOException, TimeoutException {
// 创建一个到服务器的连接
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername("yst");
factory.setPassword("yst");
factory.setHost("192.168.17.64");
Connection conn = factory.newConnection();
Channel channel = conn.createChannel();
//发布消息到我们的exchange,而不是队列
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
// 发送的消息
String message = "Hello World.";
// 发消息
channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, null, message.getBytes());
System.out.println("Sent:" + ROUTING_KEY + ":" + message);
// 关闭渠道和连接;
channel.close();
conn.close();
}
}
Consumer.java
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Consumer {
private final static String EXCHANGE_NAME = "direct_logs";
private final static String ROUTING_KEY = "key1";
public static void main(String[] args) throws IOException, TimeoutException {
// 创建一个到服务器的连接
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername("yst");
factory.setPassword("yst");
factory.setHost("192.168.17.64");
Connection conn = factory.newConnection();
Channel channel = conn.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
//获取channel绑定的队列的名字
String queueName = channel.queueDeclare().getQueue();
//将queue与exchange绑定
channel.queueBind(queueName, EXCHANGE_NAME, ROUTING_KEY);
System.out.println("Waiting for messages.");
// 创建队列消费者
final DefaultConsumer 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("Received:" + envelope.getRoutingKey() + "':'" + message);
}
};
channel.basicConsume(queueName, true, consumer);
}
}
Consumer2.java
将Consumer.java复制一份,命名为Consumer2.java。将其中的ROUTING_KEY改为“key2”。
运行两个Consumer实例,称其为C1和C2,运行一个Consumer2实例,称其为C3。运行Producer发送消息。观察运行结果
C1:
Waiting for messages.
Received:key1':'Hello World.
C2:
Waiting for messages.
Received:key1':'Hello World.
C3:
Waiting for messages.
结果验证了上述直接交换和多个绑定中的内容。
在上文中,我们改进了我们的日志系统。我们使用了直接交换,使Consumer支持选择性地接收日志。尽管如此,它仍然有局限性。为了提高日志系统中的灵活性,我们需要了解更复杂的话题交换。
topic可以对Routing key进行模式匹配,比如key*可以传递到Routing key为key1、key2、key3的Queue。Routing key的值是有限制的,它必须是由“.”分隔的单词列表,比如“ stock.usd.nyse ”,“ nyse.vmw ”,“ quick.orange.rabbit ”。Routing key最长不能超过255 bytes。
Routing key中可能有两个特殊字符:
请看下面一个例子
在这里我们创建了两个绑定: Queue1 的binding key 是".orange."; Queue2的binding key 是 “..rabbit” 和 “lazy.#”。
比如Routing key是 “quick.orange.rabbit"将会发送到Queue1和Queue2中。消息"lazy.orange.elephant” 也会发送到Queue1和Queue2。但是"quick.orange.fox" 会发送到Queue1;"lazy.brown.fox"会发送到Queue2。“lazy.pink.rabbit” 也会发送到Queue2,但是尽管两个routing_key都匹配,它也只是发送一次。“quick.brown.fox” 会被丢弃。
Topic exchange功能强大,可以转化为其他的exchange。
如果binding_key 是 “#” ,它会接收所有的Message,不管Routing key是什么,就像是fanout exchange。
如果 "*“和”#"没有被使用,那么topic exchange就变成了direct exchange。
参考资料: