RabbitMQ札记-消息分发策略

今天来学习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将接收并打印消息。

fanoutx

在前面的文章中,每个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。
RabbitMQ札记-消息分发策略_第1张图片

direct

在上文中,我们创建了一个简单的日志系统,它可以将消息广播给多个Consumer。在本文中,我们将通过direct模式为其添加一个功能。Consumer将只能订阅一部分消息。

direct意为如果Routing key匹配, 那么Message就会被传递到相应的Queue中。比如Routing key为key的Exchange可以分发消息到Routing key为key的Queue。
RabbitMQ札记-消息分发策略_第2张图片
从图中我们可以看到两个队列绑定的直接交换。第一个队列用绑定键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来说,这个参数是被忽略的。

多个绑定

绑定多个队列是完全合法的。
RabbitMQ札记-消息分发策略_第3张图片
在这种情况下,直接交换就像广播一样,将消息广播到所有的匹配队列。

最终版本

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.

结果验证了上述直接交换和多个绑定中的内容。

topic

在上文中,我们改进了我们的日志系统。我们使用了直接交换,使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中可能有两个特殊字符:

  • *可以代替一个字。
  • #可以代替零个或多个单词。

请看下面一个例子
RabbitMQ札记-消息分发策略_第4张图片
在这里我们创建了两个绑定: Queue1 的binding key 是".orange."; Queue2的binding key 是 “..rabbit” 和 “lazy.#”。

  • Queue1 对所有orange颜色的动物感兴趣
  • Queue2 对所有的rabbits和所有的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。

参考资料:

  • http://www.rabbitmq.com/tutorials/tutorial-three-java.html
  • http://www.rabbitmq.com/tutorials/tutorial-four-java.html
  • http://www.rabbitmq.com/tutorials/tutorial-five-java.html

你可能感兴趣的:(RabbitMQ)