【更多资讯及资料获取,关注微信公众号号:浅醉JAVA】
消息中间件-RabbitMQ
一 RabbitMQ简介
MQ全程为Message Queue,消息队列(MQ)是 一种应用程序对应用程序的通信方法。应用程序通过读写队列中的消息来通信,而无需专用连接来连接他们。消息传递指的是程序之间再消息中发送数据进行通信,而不是通过直接调用彼此来通信。
MQ核心理念:实现应用程序之间的异步交互,另外还可以实现应用解耦、流量削峰等,可应用与解决高并发问题。
当前流行的MQ有ActiveMQ、RabbitMQ、Kafka等,其中RabbitMQ是一个在AMQP基础上完成的,可复用的企业消息系统。
AMQP,即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息中间件设计。
二 RabbitMQ安装
安装只需要注意一点,就是需要先安装ErLang语言环境,在进行RabbiMQ的安装。具体安装方法,后期介绍。
三 RabbitMQ六种队列模式
1 点对点模式(简单模式):一个生产者上产的消息有一个消费者消费,消费完成即将该消息从队列中清除。
2 工作模式:一个生产者,对应着多个消费者,默认是生产者发布消息到队列中,RabbitMQ进行在消费者间轮询派发消息,同样保证一个消息只被消费一次。
3 发布订阅模式:生产者生产消息,发布到队列中国,与以上两者不同的是,该模式会将消息广播给所有订阅的消费者,也就是每个消费者都会得到相同的一套消息。
4 路由模式:是在发布与订阅模式的基础上演变的,通过发布消失时,设置密钥,与订阅者接收消息时绑定密钥,只有密钥匹配的消费者才能获取消息。
5 主题模式:是在路由模式上进行更细粒度的分级。将密钥是由多个单词被点连接组成。(具体看实战代码)
6 RPC模式
以上六种模式,会具体进行前五种的代码实战操作,第六种暂不做解释。
四 代码实战
关键词先知
1 工作队列(又称:任务队列)
背后的主要思想是避免立即执行资源密集型任务,并且必须等待它完成。相反,我们安排任务稍后完成。我们将任务封装 为消息并将其发送到队列。在后台运行的工作进程将弹出任务并最终执行作业。当您运行许多工作程序时,它们之间将共享任务。
2 循环调度(默认)
使用任务队列的一个优点是能够轻松地并行工作。如果我们正在积压工作积压,我们可以添加更多工人,这样就可以轻松扩展。
首先,让我们尝试同时运行两个worker实例。他们都会从队列中获取消息,但究竟如何呢?让我们来看看。
你需要打开三个控制台。两个将运行工作程序。这些游戏机将成为我们的两个消费者 - C1和C2。
#shell 1 java -cp $ CP Worker
#=> []等待消息。要退出按CTRL + C.
#shell 2 java -cp $ CP Worker
#=> []等待消息。要退出按CTRL + C.
在第三个中,我们将发布新任务。启动消费者后,您可以发布一些消息:
#shell 3
java -cp $ CP NewTask First message。
#=> [x]发送“第一条消息”。
java -cp $ CP NewTask第二条消息…
#=> [x]发送’第二条消息…’
java -cp $ CP NewTask第三条消息…
#=> [x]发送’第三条消息…’
java - cp $ CP NewTask第四条消息…
#=> [x]发送’第四条消息…’
java -cp $ CP NewTask第五条消息…
#=> [x]发送’第五条消息… …”
让我们看看交给我们工人的是什么:
java -cp $ CP Worker
#=> []正在等待消息。要退出,请按CTRL + C
#=> [x]收到“第一条消息”。
#=> [x]收到’第三条消息…’
#=> [x]收到’第五条消息…’
java -cp $ CP Worker
#=> []正在等待消息。要退出按CTRL + C
#=> [x]收到’第二条消息…’
#=> [x]收到’第四条消息…’
默认情况下,RabbitMQ将按顺序将每条消息发送给下一个消费者。平均而言,每个消费者将获得相同数量的消息。这种分发消息的方式称为循环法。与三个或更多工人一起尝试。
3 消息确认ACK(签收策略):
// 注册消费者 参数2 是手动签收(即消息确认)
boolean ackBool = false;
channel.basicConsume(QUEUE_NAME,ackBool,consumer);
确认方式:
channel.basicAck(envelope.getDeliveryTag(),false);
执行任务可能需要几秒钟。您可能想知道如果其中一个消费者开始执行长任务并且仅在部分完成时死亡会发生什么。使用我们当前的代码,一旦RabbitMQ向客户发送消息,它立即将其标记为删除。在这种情况下,如果你杀死一个工人,我们将丢失它刚刚处理的消息。我们还将丢失分发给这个特定工作者但尚未处理的所有消息。
但我们不想失去任何任务。如果工人死亡,我们希望将任务交付给另一名工人。
为了确保消息永不丢失,RabbitMQ支持 消息确认。消费者发回ack(nowledgement)以告知RabbitMQ已收到,处理了特定消息,并且RabbitMQ可以自由删除它。
如果消费者死亡(其通道关闭,连接关闭或TCP连接丢失)而不发送确认,RabbitMQ将理解消息未完全处理并将重新排队。如果同时有其他在线消费者在线,则会迅速将其重新发送给其他消费者。这样你就可以确保没有消息丢失,即使工人偶尔会死亡。
没有任何消息超时; 当消费者死亡时,RabbitMQ将重新发送消息。即使处理消息需要非常长的时间,也没关系。
默认情况下,手动消息确认已打开。在前面的示例中,我们通过autoAck = true 标志明确地将它们关闭。一旦我们完成任务,就应该将此标志设置为false并从工作人员发送适当的确认。
使用此代码,我们可以确定即使您在处理消息时使用CTRL + C杀死一名工作人员,也不会丢失任何内容。工人死后不久,所有未经确认的消息将被重新传递。
确认必须在收到的交付的同一信道上发送。
4 被遗忘的确认
错过basicAck是一个常见的错误。这是一个简单的错误,但后果是严重的。当您的客户端退出时,消息将被重新传递(这可能看起来像随机重新传递),但RabbitMQ将会占用越来越多的内存,因为它无法释放任何未经处理的消息。
为了调试这种错误,您可以使用rabbitmqctl 来打印messages_unacknowledged字段:
sudo rabbitmqctl list_queues name messages_ready messages_unacknowledged
在Windows上,删除sudo:
rabbitmqctl.bat list_queues name messages_ready messages_unacknowledged
5 消息持久性(durable)
我们已经学会了如何确保即使消费者死亡,任务也不会丢失。但是如果RabbitMQ服务器停止,我们的任务仍然会丢失。
当RabbitMQ退出或崩溃时,它将忘记队列和消息,除非你告诉它不要。确保消息不会丢失需要做两件事:我们需要将队列和消息都标记为持久。
首先,我们需要确保RabbitMQ永远不会丢失我们的队列。为此,我们需要声明它是持久的:
boolean durable = true ;
channel.queueDeclare(“hello”,durable,false,false,null);
虽然此命令本身是正确的,但它在我们当前的设置中不起作用。那是因为我们已经定义了一个名为hello的队列 ,这个队列不耐用。RabbitMQ不允许您使用不同的参数重新定义现有队列,并将向尝试执行此操作的任何程序返回错误。但是有一个快速的解决方法 - 让我们声明一个具有不同名称的队列,例如task_queue:
boolean durable = true ;
channel.queueDeclare(“task_queue”,durable,false,false,null);
此queueDeclare更改需要应用于生产者和消费者代码。
此时我们确信即使RabbitMQ重新启动,task_queue队列也不会丢失。现在我们需要将消息标记为持久性 - 通过将MessageProperties(实现BasicProperties)设置为值PERSISTENT_TEXT_PLAIN。
import com.rabbitmq.client.MessageProperties;
channel.basicPublish(“”,“task_queue”,
MessageProperties.PERSISTENT_TEXT_PLAIN,
message.getBytes());
有关消息持久性的注释
将消息标记为持久性并不能完全保证消息不会丢失。虽然它告诉RabbitMQ将消息保存到磁盘,但是当RabbitMQ接受消息并且尚未保存消息时,仍然有一个短时间窗口。此外,RabbitMQ不会为每条消息执行fsync(2) - 它可能只是保存到缓存而不是真正写入磁盘。持久性保证不强,但对于我们简单的任务队列来说已经足够了。如果您需要更强的保证,那么您可以使用 发布者确认。
6 公平派遣(channel.basicQos(1))
您可能已经注意到调度仍然无法完全按照我们的意愿运行。例如,在有两个工人的情况下,当所有奇怪的消息都很重,甚至消息很轻时,一个工人将经常忙碌而另一个工作人员几乎不会做任何工作。好吧,RabbitMQ对此一无所知,仍然会均匀地发送消息。
发生这种情况是因为RabbitMQ只是在消息进入队列时调度消息。它不会查看消费者未确认消息的数量。它只是盲目地向第n个消费者发送每个第n个消息。
为了打败我们可以使用basicQos方法和 prefetchCount = 1设置。这告诉RabbitMQ不要一次向一个worker发送一条消息。或者,换句话说,在处理并确认前一个消息之前,不要向工作人员发送新消息。相反,它会将它发送给下一个仍然不忙的工人。
int prefetchCount = 1 ;
channel.basicQos(prefetchCount);
7 保证持久性生效
保证RabbitMQ重新启动,持久性选项也可以使任务生效。
使用 消息确认 和prefetchCount(关闭公平派遣),设置工作队列。
1 点对点模式
主要步骤:
== 创建连接工程
== 创建连接
== 创建通道
== 声明队列
== 发送消息/接收消息
maven依赖
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.4.3</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
</dependency>
发布者代码:
package cn.htb;
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 Send {
public static Connection getConnection() throws IOException, TimeoutException {
// 1 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
// 2 设设服务器位置 ip+port+虚拟主机
factory.setHost("127.0.0.1");
factory.setPort(5672);
factory.setVirtualHost("/admin_host");
// 3 设置用户名 和 密码
factory.setUsername("admin");
factory.setPassword("admin");
// 4 创建并返回新的连接
return factory.newConnection();
}
private final static String QUEUE_NAME = "hello";
public static void main(String[] argv) throws Exception {
//创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
//设置主机
factory.setHost("127.0.0.1");
factory.setPort(5672);
//创建连接
Connection connection = factory.newConnection();
//创建通道
Channel channel = connection.createChannel();
//声明队列,如果队列存在,则什么都不做,如果不存在则创建
//参数1 队列名称
//参数2 是否持久化队列,我们的队列模式是在内存中的,如果rabbitmq重启会丢失数据
//如果为true,则会保存到erlang自带的数据库中
//参数3 是否排外,有两个作用:
// 作用一是当我们的连接关闭后是否自动删除队列,
// 作用二是否私有当前队列,如果私有了,其他通道不可以访问。
//参数4 是否自动删除
//参数5 我们的一些其他参数
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
//创建消息
String message = "Hello World!";
//发送消息
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println("消息发送完毕");
//关闭通道
channel.close();
//关闭连接
connection.close();
}
}
消费者代码:
package cn.htb;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Rec {
private final static String QUEUE_NAME = "hello";
public static void main(String[] args) throws IOException, TimeoutException {
//创建工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
//创建连接
Connection connection = factory.newConnection();
//创建通道
Channel channel = connection.createChannel();
//声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
//定义一个消费者
//DefaultConsumer consumer = new DefaultConsumer(channel);
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println("接收的消息是: " + message + "'");
};
//接收消息, 参数2 是自动签收
channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> {
});
}
}
注意:不要关闭消费者的通道和连接:通过这样做,我们只需让程序继续运行,关闭所有内容,然后退出!这将是尴尬的,因为我们希望在消费者异步收听消息到达时,该进程保持活动状态。
2 工作模式
maven依赖如上:
发送者代码:
package cn.htb.testWorkQueue;
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 Send {
private final static String QUEUE_NAME = "testWorkQueues";
public static void main(String[] argv) throws Exception {
//创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
//创建连接
Connection connection = factory.newConnection();
//创建通道
Channel channel = connection.createChannel();
//声明队列,如果队列存在,则什么都不做,如果不存在则创建
//参数1 队列名称
//参数2 是否持久化队列,我们的队列模式是在内存中的,如果rabbitmq重启会丢失数据
//如果为true,则会保存到erlang自带的数据库中
//参数3 是否排外,有两个作用:
// 作用一是当我们的连接关闭后是否自动删除队列,
// 作用二是否私有当前队列,如果私有了,其他通道不可以访问。
//参数4 是否自动删除
//参数5 我们的一些其他参数
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
//创建消息
for (int i = 0; i < 100; i++) {
//发送消息
channel.basicPublish("", QUEUE_NAME, null, ("生产者发送的消息: "+i).getBytes());
}
System.out.println("消息发送完毕");
//关闭通道
channel.close();
//关闭连接
connection.close();
}
}
消费者代码:
package cn.htb.testWorkQueue;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class recv {
private static String QUEUE_NAME="testWorkQueues";
public static void main(String[] args) throws IOException, TimeoutException {
//1 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
//2 创建连接
Connection connection = factory.newConnection();
//3 创建通道
Channel channel = connection.createChannel();
// 设置通道:告知RabbitMQ我的工作没有完成时不要再给我派发任务
// 当有多个消费者时:RabbitMQ默认是轮询机制发派任务,就是给消费者平均派发,如果某些机器性能不好,就会都到影响,处理较慢
// 所以,修改该模式如下:不再轮询,告知RabbitMQ我的工作没有完成时不要再给我派发任务,如此,性能好的机器就能多派发任务
channel.basicQos(1);
//4 声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//5 接收消息
//创建消费者,并重写接收方法:该方法自动循环等待
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//参数1 是消费者标签,区分不同的消费者
//参数2 是信封,可用于签收入参
//参数3 一些属性
//参数4 就是接收消息的主题
String s = new String(body);
System.out.println("消费者 2 接收的消息: "+s);
//7 签收消息
//参数2 false表示签收,true表示拒收
channel.basicAck(envelope.getDeliveryTag(),false);
}
};
//6 注册消费者 参数2 是签收策略 true自动签收 false手动签收
boolean ackBool = false;
channel.basicConsume(QUEUE_NAME,ackBool,consumer);
System.out.println("消费者2 启动成功");
}
}
3 发布与订阅模式
关键词:
1 交换机:Exchanges
在本教程的前几部分中,我们向队列发送消息和从队列接收消息。现在是时候在Rabbit中引入完整的消息传递模型了。
让我们快速回顾一下前面教程中介绍的内容:
生产者是发送消息的用户的应用程序。
队列是存储消息的缓冲器。
消费者是接收消息的用户的应用程序。
RabbitMQ中消息传递模型的核心思想是生产者永远不会将任何消息直接发送到队列。实际上,生产者通常甚至不知道消息是否会被传递到任何队列。
相反,生产者只能向交换器发送消息。交换是一件非常简单的事情。一方面,它接收来自生产者的消息,另一方面将它们推送到队列。交换器必须确切知道如何处理收到的消息。它应该附加到特定队列吗?它应该附加到许多队列吗?或者它应该被丢弃。其规则由交换类型定义。
交换类型
可供选择:direct, topic, headers and fanout(转发,主题,标题 和扇出)。
direct 和路由模式有关
topic 和主题模式有关
fanout 和发布订阅模式有关
我们将专注于最后一个 - fanout。让我们创建一个这种类型的交换,并将其称为日志:
channel.exchangeDeclare(“logs”,“fanout”);
fanout交换非常简单。正如您可以从名称中猜到的那样,它只是将收到的所有消息广播到它知道的所有队列中。而这正是我们记录器所需要的。
Listing exchanges
要列出服务器上的交换,您可以运行有用的rabbitmqctl:
sudo rabbitmqctl list_exchanges
在此列表中将有一些amq。*交换和默认(未命名)交换。这些是默认创建的,但目前您不太可能需要使用它们。
无名交换器(Nameless exchange)
在本教程的前几部分中,我们对交换一无所知,但仍能够向队列发送消息。这是可能的,因为我们使用的是默认交换,我们通过空字符串(“”)来识别。
回想一下我们之前是如何发布消息的:
channel.basicPublish(“”,“hello”,null,message.getBytes());
第一个参数是交换的名称。空字符串表示默认或无名交换:消息被路由到具有routingKey指定名称的队列(如果存在)。
现在,我们可以发布到我们的命名交换:
channel.basicPublish(“logs”,“”,null,message.getBytes());
2 临时队列
您可能还记得以前我们使用过具有特定名称的队列(还记得hello和task_queue吗?)。能够命名队列对我们来说至关重要 - 我们需要将工作人员指向同一个队列。当您想要在生产者和消费者之间共享队列时,为队列命名很重要。
但我们的日志记录器并非如此。我们希望了解所有日志消息,而不仅仅是它们的一部分。我们也只对当前流动的消息感兴趣,而不是旧消息。要解决这个问题,我们需要两件事。
首先,每当我们连接到Rabbit时,我们都需要一个新的空队列。为此,我们可以使用随机名称创建队列,或者更好 - 让服务器为我们选择随机队列名称。
其次,一旦我们断开消费者,就应该自动删除队列。
在Java客户端中,当我们没有向queueDeclare()提供参数时,我们 使用生成的名称创建一个非持久的,独占的自动删除队列:
String queueName = channel.queueDeclare()。getQueue();
您可以在队列指南中了解有关独占标志和其他队列属性的更多信息。
此时,queueName包含一个随机队列名称。例如,它可能看起来像amq.gen-JzTY20BRgKO-HjmUJj0wLg。
3 绑定(订阅者,需要将交换器和队列绑定)
我们已经创建了一个扇出交换和一个队列。现在我们需要告诉交换机将消息发送到我们的队列。交换和队列之间的关系称为绑定。
channel.queueBind(queueName,“logs”,“”);
从现在开始,日志交换会将消息附加到我们的队列中。
列出绑定
您可以使用,您猜对了,列出现有绑定
rabbitmqctl list_bindings
发布者
主要步骤:
== 创建连接工厂
== 创建连接
== 创建通道
== 声明交换器(使用临时队列,不再声明队列)
package cn.htb.publishAndSubscribe;
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 Publish {
//交换器名字
private static String EXCHANGE_NAME="logs";
public static void main(String[] args) throws IOException, TimeoutException {
// 1 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
// 2 创建连接
Connection connection = factory.newConnection();
// 3 创建通道
//此处使用临时队列,所以不再声明,而又rabbit自动创建
Channel channel = connection.createChannel();
// 4 创建交换器
//参数1 交换器的名字 参数2 交换器类型 :fanout 也就是发布订阅模式
channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
// 5 发布消息
for (int i = 0; i < 100; i++) {
// 发布订阅模式,因为消息是先发送到交换机中,而交换机是没有保存功能的,所以如果没有消费者,消息会丢失
channel.basicPublish(EXCHANGE_NAME,"",null,("发布者发布的消息: "+i).getBytes());
}
// 6 关闭通道 和 连接
channel.close();
connection.close();
System.out.println("消息发布完毕。。");
}
}
消费者:
主要步骤:
== 创建连接工厂
== 创建连接
== 创建通道
== 声明交换器
== 通过通道获取临时队列名称 String queueName = channel.queueDeclare().getQueue();
== 绑定交换器和队列 channel.queueBind(queueName, EXCHANGE_NAME, “”);
package cn.htb.publishAndSubscribe;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.Enumeration;
import java.util.concurrent.TimeoutException;
public class Subscribe {
//交换器名字
private static String EXCHANGE_NAME="logs";
public static void main(String[] args) throws IOException, TimeoutException {
// 1 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
// 2 创建连接
Connection connection = factory.newConnection();
// 3 创建通道
Channel channel = connection.createChannel();
// 4 声明交换器 参数1 交换器的名字 参数2 交换器类型 :fanout 也就是发布订阅模式
AMQP.Exchange.DeclareOk fanout = channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
// 获取临时队列名称
String queueName = channel.queueDeclare().getQueue();
// 绑定交换器和队列
channel.queueBind(queueName, EXCHANGE_NAME, "");
// 5 创建消费者
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String s = new String(body);
System.out.println("订阅者 1 接收到消息: "+s);
// 确认消息
channel.basicAck(envelope.getDeliveryTag(),false);
}
};
// 6 注册消费者 参数2 手动签收
channel.basicConsume(queueName,false,consumer);
System.out.println("订阅者启动成功。。");
}
}
4 路由模式
关键词:
绑定
在前面的例子中,我们已经在创建绑定。您可能会记得以下代码:
channel.queueBind(queueName,EXCHANGE_NAME,“”);
绑定是交换机和队列之间的关系。这可以简单地理解为:队列对来自此交换机的消息感兴趣。
绑定可以采用额外的routingKey参数。为了避免与basic_publish参数混淆,我们将其称为 绑定密钥。这就是我们如何使用键创建绑定:
channel.queueBind(queueName,EXCHANGE_NAME,“black”);
绑定密钥的含义取决于交换机类型。我们之前使用的 扇出交换只是忽略了它的价值。
转发交换机
我们上一个教程中的日志记录系统向所有消费者广播所有消息。我们希望扩展它以允许根据消息的严重性过滤消息。例如,我们可能需要一个程序将日志消息写入磁盘以仅接收严重错误,而不是在警告或信息日志消息上浪费磁盘空间。
我们使用的是扇出交换,它没有给我们太大的灵活性 - 它只能进行无意识的广播。
我们将使用转发交换。转发交换背后的路由算法很简单 - 消息进入队列,其 绑定密钥与消息的路由密钥完全匹配。
为了说明这一点,请考虑以下设置:
在此设置中,我们可以看到转发交换X与两个绑定到它的队列。第一个队列绑定橙色绑定,第二个绑定有两个绑定,一个绑定密钥为黑色,另一个绑定为绿色。
在这样的设置中,使用路由密钥orange发布到交换机的消息 将被路由到队列Q1。路由键为黑色 或绿色的消息将转到Q2。所有其他消息将被丢弃。
多个绑定
使用相同的绑定密钥绑定多个队列是完全合法的。在我们的例子中,我们可以在X和Q1之间添加绑定键黑色的绑定。在这种情况下,转发交换将表现得像扇出一样,并将消息广播到所有匹配的队列。路由密钥为黑色的消息将传送到 Q1和Q2。
发送日志
我们将此模型用于我们的日志系统。我们会将消息发送给转发交换,而不是扇出。我们将提供日志严重性作为路由密钥。这样接收程序将能够选择它想要接收的严重性。让我们首先关注发送日志。
一如既往,我们需要先创建一个交换:
channel.exchangeDeclare(EXCHANGE_NAME,“direct”);
我们已准备好发送消息:
channel.basicPublish(EXCHANGE_NAME,severity,null,message.getBytes());
为简化起见,我们假设“严重性”可以是“信息”,“警告”,“错误”之一。
订阅
接收消息将像上一个教程一样工作,但有一个例外 - 我们将为我们感兴趣的每个严重性创建一个新的绑定。
String queueName = channel.queueDeclare().getQueue();
for(String severity:argv){
channel.queueBind(queueName,EXCHANGE_NAME,severity);
}
发布者:
package cn.htb.RoutingQueue;
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 Publish {
//交换机名称
private static String EXCHANGE_NAME= "testRouting";
public static void main(String[] args) throws IOException, TimeoutException {
// 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
//创建连接
Connection connection = factory.newConnection();
//创建通道
Channel channel = connection.createChannel();
//声明交换机 参数: 交换机类型,转发交换机
channel.exchangeDeclare(EXCHANGE_NAME,"direct");
//发布消息 :参数2 绑定的密钥 只有与之相匹配的队列能够接收消息
channel.basicPublish(EXCHANGE_NAME,"key1",null,"发布者发布消息key1".getBytes());
//关闭通道和连接
channel.close();
connection.close();
System.out.println("发布成功");
}
}
接收者:
package cn.htb.RoutingQueue;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Subscribe {
//交换机名称
private static String EXCHANGE_NAME= "testRouting";
public static void main(String[] args) throws IOException, TimeoutException {
// 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
//创建连接
Connection connection = factory.newConnection();
//创建通道
Channel channel = connection.createChannel();
//声明交换机
channel.exchangeDeclare(EXCHANGE_NAME,"direct");
//获取队列名
String queueName = channel.queueDeclare().getQueue();
//绑定
channel.queueBind(queueName,EXCHANGE_NAME,"key3");
//创建消费中
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println(new String(body));
}
};
//注册消费者
channel.basicConsume(queueName,true,consumer);
System.out.println("消费者启动成功");
}
}
5 主题模式
关键词
主题交换机
发送到主题交换的消息不能是随意的 routing_key - 它必须是由点分隔的单词列表。单词可以是任何内容,但通常它们指定与消息相关的一些功能。一些有效的路由密钥示例:“ stock.usd.nyse ”,“ nyse.vmw ”,“ quick.orange.rabbit”。路由密钥中可以包含任意数量的单词,最多可达255个字节。
绑定密钥也必须采用相同的形式。主题交换背后的逻辑 类似于直接交换- 使用特定路由密钥发送的消息将被传递到与匹配绑定密钥绑定的所有队列。但是,绑定键有两个重要的特殊情况:
• *(星号)可以替代一个单词。
• #(hash)可以替换零个或多个单词。
在一个例子中解释这个是最容易的:
在这个例子中,我们将发送所有描述动物的消息。消息将与包含三个单词(两个点)的路由键一起发送。路由键中的第一个单词将描述速度,第二个是颜色,第三个是物种:“ 。。 ”。
我们创建了三个绑定: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 ”或“ quick.orange.male.rabbit ”,会发生什么?好吧,这些消息将不匹配任何绑定,将丢失。
另一方面,“ lazy.orange.male.rabbit ”,即使它有四个单词,也会匹配最后一个绑定,并将被传递到第二个队列。
与其他交换机区别
主题交换功能强大,可以像其他交易所一样。
当队列与“ # ”(哈希)绑定密钥绑定时 - 它将接收所有消息,而不管路由密钥 - 如扇出交换机。
当特殊字符“ * ”(星号)和“ # ”(哈希)未在绑定中使用时,主题交换的行为就像转换交换机一样。
发布者:
package cn.htb.TopicExchange;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Subscribe {
//交换机名称
private static String EXCHANGE_NAME = "test_topic";
public static void main(String[] args) throws IOException, TimeoutException {
// 1 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
// 2 创建连接
Connection connection = factory.newConnection();
// 3 创建通道
Channel channel = connection.createChannel();
// 4 声明交换机
channel.exchangeDeclare(EXCHANGE_NAME,"topic");
// 5 获取队列名称
String queueName = channel.queueDeclare().getQueue();
// 6 绑定交换机 quick.orange.rabbit
channel.queueBind(queueName,EXCHANGE_NAME,"quick.red.rabbit");
// 7 创建消费者
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println(new String(body));
}
};
// 8 注册消费者
channel.basicConsume(queueName,true,consumer);
}
}
接收者:
package cn.htb.TopicExchange;
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 Publish {
//交换机名称
private static String EXCHANGE_NAME = "test_topic";
public static void main(String[] args) throws IOException, TimeoutException {
// 1 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
// 2 创建连接
Connection connection = factory.newConnection();
// 3 创建通道
Channel channel = connection.createChannel();
// 4 声明交换机
channel.exchangeDeclare(EXCHANGE_NAME,"topic");
// 5 发送消息
channel.basicPublish(EXCHANGE_NAME,"quick.orange.rabbit",null,"发布者发布的消息".getBytes());
// channel.basicPublish(EXCHANGE_NAME,"*.orange.rabbit",null,"发布者发布的消息".getBytes());
// channel.basicPublish(EXCHANGE_NAME,"quick.#",null,"发布者发布的消息".getBytes());
// 6 关闭通道和连接
channel.close();
connection.close();
}
}
【更多资讯及资料获取,关注微信公众号号:浅醉JAVA】