RabbitMQ使用教程(官网翻译)
介绍
RabbitMQ是消息代理:它接受并转发消息。您可以将其视为邮局:将您要发布的邮件放在邮箱中时,可以确保Mailperson先生或女士最终将邮件传递给收件人。以此类推,RabbitMQ是一个邮箱,一个邮局和一个邮递员。
RabbitMQ与邮局之间的主要区别在于,它不处理纸张,而是接收,存储和转发数据消息的二进制斑点。
1. 基本队列(Hello World)
做某事 的最简单的事情
生产者
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
public class Send {
private final static String QUEUE_NAME = "hello";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 发送消息
String message = "Hello World!";
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
}
}
}
消费者
public class Recv {
private final static String QUEUE_NAME = "hello";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
// 监听消息
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
};
channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> { });
}
}
2. 工作队列(Work queues)
在工人之间分配任务(竞争的消费者模式)
在第一个教程中,我们编写了程序来从命名队列发送和接收消息。在这一节中,我们将创建一个工作队列,该队列将用于在多个工作人员之间分配耗时的任务。
工作队列(又称为任务队列)的主要思想是避免立即执行资源密集型任务,而不得不等待它完成。相反,我们安排任务在以后完成。我们将任务封装 为消息并将其发送到队列。在后台运行的工作进程将弹出任务并最终执行作业。当您运行许多工作人员时,任务将在他们之间共享。
这个概念在Web应用程序中特别有用,因为在Web应用程序中,不可能在较短的HTTP请求窗口内处理复杂的任务。
准备
在本教程的上半部分,我们发送了一条包含“ Hello World!”的消息。现在,我们将发送代表复杂任务的字符串。我们没有现实世界中的任务,例如要调整大小的图像或要渲染的pdf文件,所以我们假装自己很忙-使用Thread.sleep()函数来伪造它。我们将字符串中的点数作为它的复杂度。每个点将占“工作”的一秒钟。例如,Hello ...描述的虚假任务 将花费三秒钟。
我们将略微修改上一个示例中的Send.java代码,以允许从命令行发送任意消息。该程序会将任务安排到我们的工作队列中,因此将其命名为 NewTask.java:
生产者
package cn.jjaxll.amqp.workqueue;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
public class NewTask {
private final static String TASK_QUEUE_NAME = "hello";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.123.123");
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
channel.queueDeclare(TASK_QUEUE_NAME, false, false, false, null);
for (int i = 0; i < 100; i++) {
// 发送消息
String message = String.join(" ", "Task"+i);
channel.basicPublish("", "hello", null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
}
}
}
}
消费者
package cn.jjaxll.amqp.workqueue;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;
public class Work {
private final static String TASK_QUEUE_NAME = "hello";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.123.123");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(TASK_QUEUE_NAME, false, false, false, null);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
// 监听消息
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
try {
doWork(message);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(" [x] Done");
}
};
boolean autoAck = true; // acknowledgment is covered below
channel.basicConsume(TASK_QUEUE_NAME, autoAck, deliverCallback, consumerTag -> { });
};
private static void doWork(String task) throws InterruptedException {
for (char ch: task.toCharArray()) {
if (ch == '.') Thread.sleep(1000);
}
}
}
循环调度
使用任务队列的优点之一是能够轻松并行化工作。如果我们正在积压工作,我们可以增加更多的工人,这样就可以轻松扩展。
首先,让我们尝试同时运行两个辅助实例。他们都将从队列中获取消息,但是究竟如何呢?让我们来看看。
您需要打开三个控制台。两个将运行消费者程序。。
默认情况下,RabbitMQ将按顺序将每个消息发送给下一个使用者。平均而言,每个消费者都会收到相同数量的消息。这种分发消息的方式称为循环。与三个或更多的工人一起尝试。
消息确认
完成一项任务可能需要几秒钟。您可能想知道,如果其中一个使用者开始一项漫长的任务并仅部分完成而死掉,会发生什么情况。使用我们当前的代码,RabbitMQ一旦向消费者传递了一条消息,便立即将其标记为删除。在这种情况下,如果您杀死一个工人,我们将丢失正在处理的消息。我们还将丢失所有发送给该特定工作人员但尚未处理的消息。
但是我们不想丢失任何任务。如果一个工人死亡,我们希望将任务交付给另一个工人。
为了确保消息永不丢失,RabbitMQ支持 消息确认。消费者发送回确认,以告知RabbitMQ已经接收,处理了特定的消息,并且RabbitMQ可以自由删除它。
如果使用者死了(其通道已关闭,连接已关闭或TCP连接丢失)而没有发送确认,RabbitMQ将了解消息未完全处理,并将重新排队。如果同时有其他消费者在线,它将很快将其重新分发给另一个消费者。这样,您可以确保即使工人偶尔死亡也不会丢失任何消息。
没有任何消息超时;消费者死亡时,RabbitMQ将重新传递消息。即使处理消息需要非常非常长的时间也没关系。
默认情况下,手动消息确认处于打开状态。在前面的示例中,我们通过autoAck = true 标志显式关闭了它们。现在,是时候将该标志设置为false,并在工作完成后从工作人员发送适当的确认。
channel.basicQos(1); // 队列一次只接收一条没有确认的消息
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
try {
doWork(message);
} finally {
System.out.println(" [x] Done");
// 手动确认消息
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
};
// 取消自动确认,改为手动确认
boolean autoAck = false;
channel.basicConsume(TASK_QUEUE_NAME, autoAck, deliverCallback, consumerTag -> { });
讯息持久性
我们已经学会了如何确保即使消费者死亡,任务也不会丢失。但是,如果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)-它可能只是保存到缓存中,而没有真正写入磁盘。持久性保证并不强,但是对于我们的简单任务队列而言,这已经绰绰有余了。如果您需要更强有力的保证,则可以使用 发布者确认。
公平派遣
您可能已经注意到,调度仍然无法完全按照我们的要求进行。例如,在有两名工人的情况下,当所有奇怪的消息都很重,甚至消息很轻时,一位工人将一直忙碌而另一位工人将几乎不做任何工作。好吧,RabbitMQ对此一无所知,并且仍将平均分派消息。
发生这种情况是因为RabbitMQ在消息进入队列时才调度消息。它不会查看消费者的未确认消息数。它只是盲目地将每第n条消息发送给第n个使用者。

为了解决这个问题,我们可以将basicQos方法与 prefetchCount = 1设置一起使用。这告诉RabbitMQ一次不要给工人一个以上的消息。换句话说,在处理并确认上一条消息之前,不要将新消息发送给工作人员。而是将其分派给不忙的下一个工作程序。
int prefetchCount = 1 ;
channel.basicQos(prefetchCount);
关于队列大小的注意
如果所有工作人员都忙,则您的队列可以填满。您将需要关注这一点,并可能会增加更多的工作人员,或者有其他一些策略。
全部放在一起
NewTask.java类的最终代码:
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.MessageProperties;
public class NewTask {
private static final String TASK_QUEUE_NAME = "task_queue";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
String message = String.join(" ", argv);
channel.basicPublish("", TASK_QUEUE_NAME,
MessageProperties.PERSISTENT_TEXT_PLAIN,
message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + message + "'");
}
}
}
还有我们的Worker.java:
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;
public class Worker {
private static final String TASK_QUEUE_NAME = "task_queue";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
final Connection connection = factory.newConnection();
final Channel channel = connection.createChannel();
channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
channel.basicQos(1);
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
try {
doWork(message);
} finally {
System.out.println(" [x] Done");
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
};
channel.basicConsume(TASK_QUEUE_NAME, false, deliverCallback, consumerTag -> { });
}
private static void doWork(String task) {
for (char ch : task.toCharArray()) {
if (ch == '.') {
try {
Thread.sleep(1000);
} catch (InterruptedException _ignored) {
Thread.currentThread().interrupt();
}
}
}
}
}
使用消息确认和prefetchCount可以设置工作队列。耐用性选项即使重新启动RabbitMQ也能使任务继续存在。
有关Channel方法和MessageProperty的更多信息,您可以在线浏览 JavaDocs。
现在,我们可以继续进行教程3,并学习如何将相同的消息传递给许多消费者。
生产非适用性免责声明
请记住,本教程和其他教程都是教程。它们一次展示一个新概念,并且可能有意过分简化了某些事情,而忽略了其他事情。例如,为简洁起见,很大程度上省略了诸如连接管理,错误处理,连接恢复,并发和度量收集之类的主题。此类简化的代码不应视为已准备就绪。
在使用您的应用程序之前,请先阅读其余文档。我们特别推荐以下指南:发布者确认和消费者确认, 生产清单和监控。
3. 发布/订阅(Publish/Subscribe)
一次向许多消费者发送消息
在上一个教程中,我们创建了一个工作队列。工作队列背后的假设是,每个任务都恰好交付给一个工人。在这一部分中,我们将做一些完全不同的事情-我们将消息传达给多个消费者。这种模式称为“发布/订阅”。
为了说明这种s模式,我们将构建一个简单的日志记录系统。它由两个程序组成-第一个程序将发出日志消息,第二个程序将接收并打印它们。
在我们的日志系统中,接收器程序的每个运行副本都将获取消息。这样,我们将能够运行一个接收器并将日志定向到磁盘。同时我们将能够运行其他接收器并在屏幕上查看日志。
本质上,已发布的日志消息将被广播到所有接收者。
交流交流
在本教程的前面部分中,我们向队列发送消息和从队列接收消息。现在是时候在Rabbit中引入完整的消息传递模型了。
让我们快速回顾一下先前教程中介绍的内容:
-
甲生产者是发送消息的用户的应用程序。
-
甲队列是一个缓冲区,用于存储消息。
-
甲消费者是接收消息的用户的应用程序。
RabbitMQ消息传递模型中的核心思想是生产者从不将任何消息直接发送到队列。实际上,生产者经常甚至根本不知道是否将消息传递到任何队列。
相反,生产者只能将消息发送到交换机。交流是一件非常简单的事情。一方面,它接收来自生产者的消息,另一方面,将它们推入队列。交换必须确切知道如何处理收到的消息。是否应将其附加到特定队列?是否应该将其附加到许多队列中?还是应该丢弃它。规则由交换类型定义 。

有几种交换类型可用:direct,topic,headers 和fanout。我们将集中讨论最后一个-扇出。让我们创建这种类型的交换,并将其称为log:
channel.exchangeDeclare("logs", "fanout");
扇出交换非常简单。正如您可能从名称中猜测的那样,它只是将接收到的所有消息广播到它知道的所有队列中。而这正是我们记录器所需要的。
上市交易所
要列出服务器上的交换,您可以运行有用的rabbitmqctl:
sudo rabbitmqctl list_exchanges
在此列表中,将有一些amq。*交换和默认(未命名)交换。这些是默认创建的,但是您现在不太可能需要使用它们。
无名交换
在本教程的前面部分中,我们对交换一无所知,但仍然能够将消息发送到队列。这是可能的,因为我们使用的是默认交换,我们通过空字符串(“”)进行标识。
回想一下我们之前如何发布消息:
channel.basicPublish(“”,“ hello”,null,message.getBytes());
第一个参数是交换的名称。空字符串表示默认或无名称交换:消息将以routingKey指定的名称路由到队列(如果存在)。
现在,我们可以改为发布到我们的命名交易所:
channel.basicPublish( "logs", "", null, message.getBytes());
临时队列
您可能还记得,我们使用的是具有特定名称的队列(还记得hello和task_queue吗?)。能够命名队列对我们来说至关重要-我们需要将工人指向同一队列。当您想在生产者和消费者之间共享队列时,给队列命名很重要。
但这不是我们的记录器的情况。我们希望听到所有日志消息,而不仅仅是它们的一部分。我们也只对当前正在发送的消息感兴趣,而对旧消息不感兴趣。为了解决这个问题,我们需要两件事。
首先,无论何时连接到Rabbit,我们都需要一个全新的空队列。为此,我们可以创建一个具有随机名称的队列,或者甚至更好-让服务器为我们选择一个随机队列名称。
其次,一旦我们断开了使用者的连接,队列将被自动删除。
在Java客户端中,当我们不向queueDeclare()提供任何参数时,我们将 使用生成的名称创建一个非持久的,排他的,自动删除的队列:
String queueName = channel.queueDeclare().getQueue();
您可以在队列指南中了解有关排他标志和其他队列属性的更多信息。
此时,queueName包含一个随机队列名称。例如,它可能看起来像amq.gen-JzTY20BRgKO-HjmUJj0wLg。
绑定

我们已经创建了一个扇出交换和一个队列。现在我们需要告诉交换机将消息发送到我们的队列。交换和队列之间的关系称为绑定。
channel.queueBind(queueName,“ logs”,“”);
从现在开始,logs交换将消息添加到我们的队列中。
列表绑定
您可以使用猜测的方式列出现有绑定,
rabbitmqctl list_bindings
全部放在一起

产生日志消息的生产程序与上一教程看起来没有太大不同。最重要的变化是我们现在希望将消息发布到日志交换器,而不是无名的消息交换器。发送时,我们需要提供一个routingKey,但是对于扇出交换,它的值将被忽略。这是EmitLog.java程序的代码 :
public class EmitLog {
private static final String EXCHANGE_NAME = "logs";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
String message = argv.length < 1 ? "info: Hello World!" :
String.join(" ", argv);
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + message + "'");
}
}
}
如您所见,建立连接后,我们声明了交换。由于禁止发布到不存在的交易所,因此此步骤是必需的。
如果没有队列绑定到交换机,则消息将丢失,但这对我们来说是可以的。如果没有消费者在听,我们可以安全地丢弃该消息。
ReceiveLogs.java的代码:
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;
public class ReceiveLogs {
private static final String EXCHANGE_NAME = "logs";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
String queueName = channel.queueDeclare().getQueue();
channel.queueBind(queueName, EXCHANGE_NAME, "");
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
}
}
结果的解释很简单:交换logs的数据进入具有服务器分配名称的两个队列。这正是我们的意图。
要了解如何侦听消息的子集,让我们继续学习 教程4
4. 路由(Routing)
有选择地接收消息
在上一教程中,我们构建了一个简单的日志记录系统。我们能够向许多接收者广播日志消息。
在本教程中,我们将向其中添加功能-我们将使仅订阅消息的子集成为可能。例如,我们将只能将严重错误消息定向到日志文件(以节省磁盘空间),同时仍然能够在控制台上打印所有日志消息。
绑定
在前面的示例中,我们已经在创建绑定。您可能会想起以下代码:
channel.queueBind(queueName, EXCHANGE_NAME, "");
绑定是交换和队列之间的关系。可以简单地理解为:队列对来自此交换的消息感兴趣。
绑定可以采用额外的routingKey参数。为了避免与basic_publish参数混淆,我们将其称为 绑定键。这是我们可以创建带有键的绑定的方法:
channel.queueBind(queueName, EXCHANGE_NAME, "black");
绑定密钥的含义取决于交换类型。我们之前使用的 扇出交换只是忽略了它的价值。
直接交换
上一教程中的日志系统将所有消息广播给所有使用者。我们想要扩展它以允许根据邮件的严重性过滤邮件。例如,我们可能希望将日志消息写入磁盘的程序仅接收严重错误,而不会在警告或信息日志消息上浪费磁盘空间。
我们使用的是扇出交换,它并没有给我们带来太大的灵活性-它只能进行无意识的广播。
我们将使用直接交换。直接交换背后的路由算法很简单-消息进入其绑定密钥与消息的路由密钥完全匹配的队列 。
为了说明这一点,请考虑以下设置:

在此设置中,我们可以看到绑定了两个队列的直接交换X。第一个队列由绑定键orange绑定,第二个队列有两个绑定,一个绑定键为black,另一个绑定为green。
在这样的设置中,将使用路由键橙色将要发布到交换机的消息 路由到队列Q1。路由键为黑色 或绿色的消息将转到Q2。所有其他消息将被丢弃。
多重绑定

用相同的绑定密钥绑定多个队列是完全合法的。在我们的示例中,我们可以使用绑定键black在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);
}
全部放在一起

EmitLogDirect.java类的代码:
package cn.jjaxll.amqp.routing;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
public class EmitLogDirect {
private static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
// 设置rabbit服务器地址
factory.setHost("192.168.123.123");
// 将创建连接放在try里面,try代码块结束之后会立刻释放资源
try (Connection connection = factory.newConnection();
// 创建通道
Channel channel = connection.createChannel()) {
// 声明交换机 交换机名称 交换机模式/直连模式
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
for (int i = 0; i < 100; i++) {
// 消息
String message = getMessage(i);
// 路由key
String severity = getSeverity(i);
channel.basicPublish(EXCHANGE_NAME, severity, null, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + severity + "':'" + message + "'");
}
}
}
private static String getSeverity(int index) {
if(index % 2 == 0){
return "info";
}else{
return "error";
}
}
private static String getMessage(int index) {
if(index % 2 == 0){
return "hello world";
}else{
return "error";
}
}
}
ReceiveLogsDirect.java类的代码:
package cn.jjaxll.amqp.routing;
import com.rabbitmq.client.*;
public class ReceiveLogsDirect {
private static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
// 设置rabbit服务器地址
factory.setHost("192.168.123.123");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
// 声明交换机 交换机名称 连接方式/直连模式
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
// 队列名称
String queueName = channel.queueDeclare().getQueue();
System.out.println(String.format("queueName%s",queueName));
// 绑定交换机到队列 队列名称 交换机名称 路由key
channel.queueBind(queueName, EXCHANGE_NAME, "error");
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
// 监听消息
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" +
delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
};
// 自动确认消息
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
}
}
ReceiveLogsDirect2.java类的代码:
package cn.jjaxll.amqp.routing;
import com.rabbitmq.client.*;
public class ReceiveLogsDirect {
private static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
// 设置rabbit服务器地址
factory.setHost("192.168.123.123");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
// 声明交换机 交换机名称 连接方式/直连模式
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
// 队列名称
String queueName = channel.queueDeclare().getQueue();
System.out.println(String.format("queueName%s",queueName));
// 绑定交换机到队列 队列名称 交换机名称 路由key
channel.queueBind(queueName, EXCHANGE_NAME, "info");
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
// 监听消息
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" +
delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
};
// 自动确认消息
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
}
}
5. 话题(topics)
根据模式(主题)接收消息
在上一个教程中,我们改进了日志记录系统。代替使用仅能进行虚拟广播的扇出交换机,我们使用直接交换机,并有选择地接收日志的可能性。
尽管使用直接交换对我们的系统进行了改进,但它仍然存在局限性-它无法基于多个条件进行路由。
在我们的日志记录系统中,我们可能不仅要根据严重性订阅日志,还要根据发出日志的源订阅日志。您可能从syslog unix工具中了解了这个概念,该 工具根据严重性(info / warn / crit ...)和工具(auth / cron / kern ...)路由日志。
这将给我们带来很大的灵活性-我们可能只想听听来自'cron'的严重错误,也可以听听'kern'的所有日志。
为了在我们的日志系统中实现这一点,我们需要了解更复杂的主题交换。
话题交流
发送到主题交换机的消息不能具有任意的 routing_key-它必须是单词列表,以点分隔。这些词可以是任何东西,但通常它们指定与消息相关的某些功能。一些有效的路由关键示例:“ stock.usd.nyse ”,“ nyse.vmw ”,“ quick.orange.rabbit ”。路由关键字中可以包含任意多个单词,最多255个字节。
绑定密钥也必须采用相同的形式。主题交换背后的逻辑 类似于直接交换-用特定路由键发送的消息将传递到所有用匹配绑定键绑定的队列。但是,绑定键有两个重要的特殊情况:
-
*(星号)可以代替一个单词。
-
#(哈希)可以替代零个或多个单词。
在一个示例中最容易解释这一点:

在此示例中,我们将发送所有描述动物的消息。将使用包含三个词(两个点)的路由密钥发送消息。路由键中的第一个单词将描述速度,第二个是颜色,第三个是物种:“ <speed>。<color>。<species> ”。
我们创建了三个绑定:Q1与绑定键“ * .orange。* ”绑定,Q2与“ 。。rabbit ”和“ lazy。# ”绑定。
这些绑定可以总结为:
-
Q1对所有橙色动物都感兴趣。
-
第2季想听听有关兔子的一切,以及有关懒惰动物的一切。
路由键设置为“ quick.orange.rabbit ”的消息将传递到两个队列。消息“ lazy.orange.elephant ”也将发送给他们两个。另一方面,“ quick.orange.fox ”只会进入第一个队列,而“ lazy.brown.fox ”只会进入第二个队列。“ lazy.pink.rabbit ”将被传递到第二队只有一次,即使两个绑定匹配。“ quick.brown.fox ”与任何绑定都不匹配,因此将被丢弃。
如果我们违反合同并发送一个或四个单词的消息,例如“橙色”或“ quick.orange.male.rabbit ”,会发生什么?好吧,这些消息将不匹配任何绑定,并且将会丢失。
另一方面,“ lazy.orange.male.rabbit ”即使有四个单词,也将匹配最后一个绑定,并将其传送到第二个队列。
话题交流
主题交流功能强大,可以像其他交流一样进行。
当队列用“ # ”(哈希)绑定键绑定时,它将接收所有消息,而与路由键无关,就像在扇出交换中一样。
当在绑定中不使用特殊字符“ * ”(星号)和“ # ”(哈希)时,主题交换的行为就像直接的一样。
全部放在一起
EmitLogTopic.java
package cn.jjaxll.amqp.topic;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
public class EmitLogTopic {
private static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
// 设置rabbit服务器地址
factory.setHost("192.168.123.123");
// 将创建连接放在try里面,try代码块结束之后会立刻释放资源
try (Connection connection = factory.newConnection();
// 创建通道
Channel channel = connection.createChannel()) {
// 声明交换机 交换机名称 交换机模式/直连模式
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
int count = 1;
for (int i = 0; i < 100; i++) {
String message = "";
String topic = "";
if(count == 1){
message = "hello lazy";
topic = "lazy.#";
count++;
}else if(count == 2){
message = "hello rabbit";
topic = "*.*.rabiit";
count++;
}else if(count == 3){
message = "hello oragin";
topic = "*.oragin.*";
count++;
}else if(count == 4) {
message = "违规的字符串";
topic = "asd.asd.adsa.asd.das";
count++;
}else {
message = "lazy违规的字符串";
topic = "lazy.asd.adsa.asd.das";
count = 1;
}
channel.basicPublish(EXCHANGE_NAME, topic, null, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + topic + "':'" + message + "'");
}
}
}
private static String getSeverity(int index) {
if(index % 2 == 0){
return "quite.oragin.rabbit";