RabbitMQ的几种工作模式
1.命名队列
Send
public class Send {
private final static String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception{
ConnectionFactory factory = RabbitMQUtil.getConnectionFactory();
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 + "'");
}
}
}
Recv
public class Recv {
private final static String QUEUE_NAME = "hello";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = RabbitMQUtil.getConnectionFactory();
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(), StandardCharsets.UTF_8);
System.out.println(" [x] Received '" + message + "'");
};
channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> { });
}
}
2.工作队列
生产者
/**
*
* 使用MessageProperties.PERSISTENT_TEXT_PLAIN也不能保证不会丢失消息,RabbitMQ不会每个消息都执行fsync(2),它会将消息放到缓存里而不是磁盘
* 需要更强的保证的话需要用到publisher confirms
*
* @author dengwei
*/
public class NewTask {
public static void main(String[] args) throws Exception {
ConnectionFactory factory = RabbitMQUtil.getConnectionFactory();
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
boolean durable = true; // 持久化设置 生产者和消费者都要设置 必须再队列第一次创建的时候声明
channel.queueDeclare(TASK_QUEUE, durable, false, false, null);
String message = "Hello world.";
// 更改消息属性 表示持久化的消息
channel.basicPublish("", TASK_QUEUE, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
}
}
}
消费者
/**
* 启动多个worker
* 默认会轮流获取数据,每个实例获取的数据量是一样的
* 手动确认消息 autoAck置为false
*/
public class Worker {
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = RabbitMQUtil.getConnectionFactory();
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
boolean durable = true; // 队列是否持久化 同生产者
channel.queueDeclare(TASK_QUEUE, durable, false, false, null);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
channel.basicQos(1); // 一次只接受一个未确认(unack-ed)的消息
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
System.out.println(" [x] Received '" + message + "'");
try {
doWork(message);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(" [x] Done");
// 确认消息
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
};
boolean autoAck = false; // acknowledgment is covered below
channel.basicConsume(TASK_QUEUE, autoAck, deliverCallback, consumerTag -> {});
}
private static void doWork(String task) throws InterruptedException {
for (char ch : task.toCharArray()) {
if (ch == '.') Thread.sleep(100);
}
}
}
3.发布/订阅
RabbitMQ消息传递模型的核心思想是生产者从不直接向队列发送任何消息。实际上,通常生产者甚至根本不知道消息是否会被发送到任何队列。
相反,生产者只能向交换器发送消息。交换是一件很简单的事情。一方面它接收来自生产者的消息,另一方面它将消息推送到队列中。交换器必须确切地知道如何处理它接收到的消息。它应该被添加到一个特定的队列中吗?它应该被追加到许多队列中吗?或者它应该被丢弃。这些规则由交换类型定义。
有一些可用的交换类型:direct、topic、header和fanout。我们来关注最后一个,fanout。让我们创建一个这种类型的交换器,命名为logs
channel.exchangeDeclare("logs", "fanout");
fanout exchange非常简单。正如您可能从名称中猜到的那样,它只是将它接收到的所有消息广播到它所知道的所有队列。这正是我们需要的。
现在,我们可以将其发布到已命名的exchange:
channel.basicPublish( "logs", "", null, message.getBytes());
我们希望了解所有日志消息,而不仅仅是其中的一个子集。我们还只对当前流动的消息感兴趣,而不是对旧消息感兴趣。要解决这个问题,我们需要两样东西。
首先,当我们连接到Rabbit时,我们需要一个新鲜的空队列。要做到这一点,我们可以创建一个随机名称的队列,或者更好的是——让服务器为我们选择一个随机的队列名称。
其次,一旦断开消费者的连接,队列就会被自动删除。
在Java客户端中,当我们没有为queueDeclare()提供参数时,我们会创建一个非持久的、独占的、自动删除的队列,并生成一个名称:
String queueName = channel.queueDeclare().getQueue();
此时,queueName包含一个随机队列名。例如,它可能看起来像amq.gen . jzty20brgko - hjmujj0wlg。
我们已经创建了fanout交换和队列。现在,我们需要告诉exchange向队列发送消息。交换器和队列之间的这种关系称为绑定。
channel.queueBind(queueName, "logs", "");
从现在开始,logs交换将向我们的队列添加消息。
可以命令行列出绑定:
rabbitmqctl list_bindings
汇总
生成器程序发出日志消息,与前面的教程没有太大的不同。最重要的变化是,我们现在希望将消息发布到日志交换,而不是无名的交换。我们需要在发送时提供一个routingKey,但是它的值在fanout交换时被忽略。下面是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 + "'");
}
}
}
如您所见,在建立连接之后,我们声明了exchange。这一步是必要的,因为发布到一个不存在的交换是被禁止的。
如果还没有队列绑定到交换器上,消息就会丢失,但这对我们来说是可以的;如果还没有消费者在监听,我们可以安全地丢弃该消息。
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 -> { });
}
}
4.路由(Routing)
在前面的教程中,我们构建了一个简单的日志系统。我们能够将日志信息广播给许多接收者。
在本教程中,我们将为它添加一个特性——我们将使订阅消息的一个子集成为可能。例如,我们将能够只将关键错误消息指向日志文件(以节省磁盘空间),同时仍然能够在控制台上打印所有的日志消息。
绑定(Bindings)
在前面的例子中,我们已经创建了绑定。你可以像这样回忆代码::
channel.queueBind(queueName, EXCHANGE_NAME, "");
绑定是交换器和队列之间的关系。这可以简单地理解为: 队列对来自此交换的消息感兴趣。
绑定可以接受一个额外的routingKey
参数。为了避免与basic_publish
参数的混淆,我们将其称为binding key
。下面是使用键创建绑定的方法:
channel.queueBind(queueName, EXCHANGE_NAME, "black");
绑定键的含义取决于交换类型。我们之前使用的fanout
交换简单地忽略了它的值。
Direct exchange
上一教程中的日志系统将所有消息广播给所有用户。我们希望对其进行扩展,以允许根据其严重性对消息进行过滤。例如,我们可能希望一个将日志消息写入磁盘的程序只接收严重错误,而不会在警告或信息日志消息上浪费磁盘空间。
我们使用的是fanout交换,这并没有给我们太多的灵活性——它只能进行无意识的广播。
我们将使用直接交换。直接交换背后的路由算法很简单——消息被发送到队列,其binding key
与消息的routing key
完全匹配。
为了说明这一点,考虑以下设置:
在这个设置中,我们可以看到绑定了两个队列的直接交换X。第一个队列的绑定键为橙色,第二个队列有两个绑定,一个绑定键为黑色,另一个绑定键为绿色。
在这样的设置中,发布给交换器的带有橙色路由键的消息将被路由到队列Q1。带有黑色或绿色路由键的消息将发送到Q2。所有其他消息将被丢弃。
多个绑定
使用相同的绑定键绑定多个队列是完全合法的。在我们的例子中,我们可以用绑定键黑色在X和Q1之间添加绑定。在这种情况下,直接交换将像fanout一样工作,并将消息广播到所有匹配的队列。带有路由键黑色的消息将同时发送到Q1和Q2。
发送日志
我们将在日志系统中使用这个模型。我们将把消息发送到直接交换,而不是fanout。我们将提供日志的严重性作为一个路由键。这样接收程序就能选择它想要接收的严重性。让我们首先关注发送日志。
一如既往,我们需要首先创建一个交换:
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
我们已经准备好传达一个信息:
channel.basicPublish(EXCHANGE_NAME, severity, null, message.getBytes());
为了简化问题,我们假设“severity”(routing key)可以是“信息”、“警告”、“错误”中的一种。
订阅
接收消息的工作方式与前面的教程类似,但有一个例外——我们将为我们感兴趣的每个严重性创建一个新的绑定。
String queueName = channel.queueDeclare().getQueue();
for(String severity : argv){
channel.queueBind(queueName, EXCHANGE_NAME, severity);
}
汇总
EmitLogDirect.java
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();
factory.setHost("localhost");
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
String severity = getSeverity(argv);
String message = getMessage(argv);
channel.basicPublish(EXCHANGE_NAME, severity, null, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + severity + "':'" + message + "'");
}
}
//..
}
ReceiveLogsDirect.java
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();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
String queueName = channel.queueDeclare().getQueue();
if (argv.length < 1) {
System.err.println("Usage: ReceiveLogsDirect [info] [warning] [error]");
System.exit(1);
}
for (String severity : argv) {
channel.queueBind(queueName, EXCHANGE_NAME, severity);
}
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 -> { });
}
}
生产(非)适用性免责声明
请记住,本教程和其他教程都是教程。他们一次展示一个新概念,可能会有意地简化一些东西,而忽略其他东西。例如,为了简洁起见,诸如连接管理、错误处理、连接恢复、并发性和度量收集等主题在很大程度上被省略了。这种简化的代码不应该被认为可以用于生产。
Topics
在之前的教程中,我们改进了日志系统。我们没有使用只能进行虚拟广播的fanout交换机,而是使用了直接交换机,从而获得了选择性接收日志的可能性。
尽管使用直接交换改进了我们的系统,但它仍然有局限性——它不能基于多个尺度进行路由。
在日志系统中,我们可能不仅希望根据严重程度订阅日志,还希望根据发出日志的源订阅日志。您可能从syslog unix工具中了解到这个概念,该工具根据严重性(info/warn/crit…)和功能范围(auth/cron/kern…)路由日志。
这将给我们提供很大的灵活性——我们可能想要监听来自'cron'的关键错误,但也想要监听来自'kern'的所有日志。
要在我们的日志系统中实现它,我们需要了解一个更复杂的topic交换。
Topic exchange
发送到主题交换的消息不能有任意的routing_key——它必须是由点分隔的单词列表。这些词可以是任何词,但通常它们指明了与信息相关的一些特征。以下是一些有效的路由关键示例: "stock.usd.nyse", "nyse.vmw", "quick.orange.rabbit"。路由键中可以有任意多的单词,最多为255个字节。
绑定键的形式也必须相同。主题交换背后的逻辑与直接交换类似——带有特定路由键的消息将被发送到所有绑定了匹配绑定键的队列。然而,绑定键有两种重要的特殊情况:
- *(星号)可以替换一个单词。
- #(哈希)可以替换零个或多个单词。
在这个例子中,我们将发送描述动物的信息。这些消息将与由三个单词(两个点)组成的路由关键字一起发送。路由键中的第一个单词将描述速度,第二个是颜色,第三个是物种: “
我们创建了三个绑定: Q1通过绑定键“*.orange.*
”进行绑定,Q2绑定“*.*.rabbit
”和“lazy.#
”。
这些绑定可以总结为:
- Q1对所有橙色动物感兴趣。
- Q2想监听所有的兔子,还有所有懒惰的动物。
quick.orange.rabbit
lazy.orange.elephant
会被分发到两个队列,quick.orange.fox
仅会到Q1,lazy.brown.fox
仅会到Q2,lazy.pink.rabbit
仅会到Q2一次,即使它匹配了两个绑定。quick.brown.fox
没有匹配到任何绑定将被丢弃。
如果我们违反约定,只发了一个或四个字的信息,会发生什么呢? 像:orange
quick.orange.male.rabbit
这些消息不匹配任何绑定,将丢失。
另一方面,lazy.orange.male.rabbit
尽管它有四个单词,但它将匹配最后一个绑定,并将被发送到第二个队列。
Topic exchange
话题交换功能强大,可以像其他交换一样工作。
当队列被绑定到“#”(哈希)绑定键时,它将接收所有消息,而不管路由键是什么——就像fanout交换一样。
当特殊字符“*”(星号)和“#”(散列)不在绑定中使用时,主题交换的行为就像直接交换一样。
汇总
我们将在日志系统中使用主题交换。我们首先假设日志的路由键有两个单词:“
代码与前一篇教程中的代码几乎相同。
EmitLogTopic.java
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();
factory.setHost("localhost");
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
String routingKey = getRouting(argv);
String message = getMessage(argv);
channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + routingKey + "':'" + message + "'");
}
}
//..
}
ReceiveLogsTopic.java
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;
public class ReceiveLogsTopic {
private static final String EXCHANGE_NAME = "topic_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, "topic");
String queueName = channel.queueDeclare().getQueue();
if (argv.length < 1) {
System.err.println("Usage: ReceiveLogsTopic [binding_key]...");
System.exit(1);
}
for (String bindingKey : argv) {
channel.queueBind(queueName, EXCHANGE_NAME, bindingKey);
}
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 -> { });
}
}
6.RPC
省略
7.发布者确认(Publisher Confirms)
Publisher confirm是RabbitMQ实现可靠发布的扩展。当在通道上启用发布者确认时,客户机发布的消息将由broker异步确认,这意味着它们将由服务器端处理。
概述
在本教程中,我们将使用publisher confirm来确保已发布的消息已安全到达broker。我们将介绍几种使用publisher confirm的策略,确认并解释它们的优缺点。
在通道上启用发布服务器确认
Publisher confirm是AMQP 0.9.1协议的RabbitMQ扩展,所以默认情况下不启用。Publisher confirm在通道级通过confirmSelect
方法启用:
Channel channel = connection.createChannel();
channel.confirmSelect();
此方法必须在您希望使用的每个通道上调用发行者确认。confirm应该只启用一次,而不是对每个发布的消息都启用。
策略#1:单独发布消息
让我们从最简单的带确认的发布方法开始,即发布消息并同步等待confirm消息:
while (thereAreMessagesToPublish()) {
byte[] body = ...;
BasicProperties properties = ...;
channel.basicPublish(exchange, queue, properties, body);
// uses a 5 second timeout
channel.waitForConfirmsOrDie(5_000);
}
在这个示例中,我们像往常一样发布消息,并使用Channel#waitForConfirmsOrDie(long)方法等待消息的确认。确认消息后,该方法将立即返回。如果消息在超时时间内没有被确认,或者消息被否定应答(这意味着代理由于某种原因不能处理它),该方法将抛出异常。异常的处理通常包括记录错误消息和/或重新尝试发送消息。
不同的客户端库有不同的方法来同步处理publisher confirm,所以一定要仔细阅读您正在使用的客户端文档。
这种技术非常简单,但也有一个主要的缺点:它显著降低了发布速度,因为对消息的确认会阻碍所有后续消息的发布。这种方法不会交付超过每秒几百条已发布消息的吞吐量。不过,对于某些应用程序来说,这已经足够好了。
发布者确认是异步的吗?
我们在开始时提到,broker以异步方式确认已发布的消息,但在第一个示例中,代码会同步等待,直到消息被确认为止。客户端实际异步接收确认,并相应地解除对waitForConfirmsOrDie调用的阻塞。可以将waitForConfirmsOrDie看作是一个同步助手,它在底层依赖于异步通知。
策略#2:批量发布消息
为了改进前面的示例,我们可以发布一批消息并等待整个批消息被确认。下面的示例使用批处理大小为100:
int batchSize = 100;
int outstandingMessageCount = 0;
while (thereAreMessagesToPublish()) {
byte[] body = ...;
BasicProperties properties = ...;
channel.basicPublish(exchange, queue, properties, body);
outstandingMessageCount++;
if (outstandingMessageCount == batchSize) {
ch.waitForConfirmsOrDie(5_000);
outstandingMessageCount = 0;
}
}
if (outstandingMessageCount > 0) {
ch.waitForConfirmsOrDie(5_000);
}
与等待单个消息的确认相比,等待一批消息的确认可以大大提高吞吐量(在远程RabbitMQ节点中,比等待单个消息的确认可以提高20-30倍)。一个缺点是,在出现故障时,我们不能确切地知道出错的地方,因此我们可能必须在内存中保存整个批处理,以记录有意义的内容或重新发布消息。这个解决方案仍然是同步的,因此它阻止了消息的发布。
策略3:异步处理发布者确认
broker会异步确认发布的消息,你只需要在客户端注册一个回调函数就可以收到这些确认的通知:
Channel channel = connection.createChannel();
channel.confirmSelect();
channel.addConfirmListener((sequenceNumber, multiple) -> {
// code when message is confirmed
}, (sequenceNumber, multiple) -> {
// code when message is nack-ed
});
有两个回调:一个用于已确认的消息,一个用于nacked消息(可能被broker认为丢失的消息)。每个回调函数有两个参数:
- sequence number: 识别确认或否定消息的数字,我们将很快看到如何将其与已发布的消息关联起来。.
- multiple: 一个布尔值,如果false,只有一个消息被确认或否决,true,表示小于等于sequence number的消息被确认或否决。
序列号可以在发布前Channel#getNextPublishSeqNo()
获取:
int sequenceNumber = channel.getNextPublishSeqNo());
ch.basicPublish(exchange, queue, properties, body);
将消息与序列号关联的一种简单方法是使用映射。假设我们希望发布字符串,因为它们很容易转换为用于发布的字节数组。下面是一个使用map将发布序列号与消息的字符串主体关联起来的代码示例:
ConcurrentNavigableMap<Long, String> outstandingConfirms = new ConcurrentSkipListMap<>();
// ... code for confirm callbacks will come later
String body = "...";
outstandingConfirms.put(channel.getNextPublishSeqNo(), body);
channel.basicPublish(exchange, queue, properties, body.getBytes());
发布代码现在使用映射跟踪出站消息。当确认到达时,我们需要清理映射,并在消息被取消时记录警告:
ConcurrentNavigableMap<Long, String> outstandingConfirms = new ConcurrentSkipListMap<>();
ConfirmCallback cleanOutstandingConfirms = (sequenceNumber, multiple) -> {
if (multiple) {
ConcurrentNavigableMap<Long, String> confirmed = outstandingConfirms.headMap(
sequenceNumber, true
);
confirmed.clear();
} else {
outstandingConfirms.remove(sequenceNumber);
}
};
channel.addConfirmListener(cleanOutstandingConfirms, (sequenceNumber, multiple) -> {
String body = outstandingConfirms.get(sequenceNumber);
System.err.format(
"Message with body %s has been nack-ed. Sequence number: %d, multiple: %b%n",
body, sequenceNumber, multiple
);
cleanOutstandingConfirms.handle(sequenceNumber, multiple);
});
// ... publishing code
这个示例包含一个回调,在确认到达时将清理映射。注意这个回调函数同时处理单个和多个确认。这个回调在确认到达时使用(作为Channel#addConfirmListener
的第一个参数)。nacked消息的回调函数获取消息体并发出警告。然后,它重新使用之前的回调来清理未执行confirm的映射(无论消息是被confirm还是nack-ed,它们在map中的对应条目都必须被删除)。
如何追踪未完成的确认?
我们的示例使用ConcurrentNavigableMap来跟踪未完成的确认。这种数据结构很方便,有几个原因。它允许轻松地将序列号与消息(无论消息数据是什么)关联起来,并轻松地清理到给定序列id的条目(以处理多个confirm /nacks)。最后,它支持并发访问,因为确认回调是在客户端库拥有的线程中调用的,该线程应该与发布线程保持不同。
除了使用复杂的映射实现之外,还有其他方法可以跟踪未完成的确认,比如使用简单的并发哈希映射和一个变量来跟踪发布序列的下界,但这些方法通常更复杂,不属于教程。
综上所述,异步处理发布者确认通常需要以下步骤:
- 提供一种将发布序列号与消息关联起来的方法。
- 在通道上注册一个确认监听器,当发布者acks/nacks到达时就会通知监听器来执行适当的操作,例如记录或重新发布一个nacks -ed消息。序列号到消息的关联机制也可能需要在此步骤中进行一些清理。
- 在发布消息之前跟踪发布序列号。
重新nack-ed消息吗?
从相应的回调中重新发布一个nacked消息是很诱人的,但是应该避免这样做,因为确认回调是在I/O线程中分派的,在这个线程中通道不应该做操作。一个更好的解决方案是将消息放入由发布线程轮询的内存队列中。像ConcurrentLinkedQueue这样的类是在确认回调和发布线程之间传输消息的好选择。
总结
在某些应用程序中,确保已发布的消息被发送到代理是至关重要的。Publisher confirm是RabbitMQ的一个特性,可以帮助满足这一需求。发布者确认在本质上是异步的,但也可以同步处理它们。没有明确的方法来实现发布者的确认,这通常归结到应用程序和整个系统的约束。典型的技术有:
- 单独发布消息,同步等待确认:很简单,但吞吐量非常有限。
- 批量发布消息,同步等待批处理的确认:简单、合理的吞吐量,但很难判断什么时候出错。
- 异步处理: 最佳的性能和资源的使用,在出现错误时良好的控制,但可能需要正确地实现。
放在一起
PublisherConfirms.java包含了我们所介绍的技术的代码。我们可以编译它,按原样执行它,然后看看它们是如何执行的:
javac -cp $CP PublisherConfirms.java
java -cp $CP PublisherConfirms
输出如下所示:
Published 50,000 messages individually in 5,549 ms
Published 50,000 messages in batch in 2,331 ms
Published 50,000 messages and handled confirms asynchronously in 4,054 ms
如果客户机和服务器位于同一台机器上,那么计算机上的输出应该类似。单独发布消息的性能不如预期,但与批量发布相比,异步处理的结果有点令人失望。
发布确认非常依赖于网络,所以我们最好尝试使用远程节点,这更现实,因为在生产中客户端和服务器通常不在同一台机器上。PublisherConfirms.java可以很容易地更改为使用非本地节点:
static Connection createConnection() throws Exception {
ConnectionFactory cf = new ConnectionFactory();
cf.setHost("remote-host");
cf.setUsername("remote-user");
cf.setPassword("remote-password");
return cf.newConnection();
}
重新编译类,再次执行它,并等待结果:
Published 50,000 messages individually in 231,541 ms
Published 50,000 messages in batch in 7,232 ms
Published 50,000 messages and handled confirms asynchronously in 6,332 ms
我们发现,独立发布现在表现糟糕。但是在客户端和服务器之间的网络中,批量发布和异步处理现在的表现类似,异步处理发布确认有小的优势。
请记住,批量发布很容易实现,但在出现负面的发布者确认时,很难知道哪些消息无法到达broker 。异步处理发布确认需要更多的实现,但当发布的消息被nack-ed时,对要执行的操作提供更好的粒度和更好的控制。
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.ConfirmCallback;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.time.Duration;
import java.util.UUID;
import java.util.concurrent.ConcurrentNavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.function.BooleanSupplier;
public class PublisherConfirms {
static final int MESSAGE_COUNT = 50_000;
static Connection createConnection() throws Exception {
ConnectionFactory cf = new ConnectionFactory();
cf.setHost("localhost");
cf.setUsername("guest");
cf.setPassword("guest");
return cf.newConnection();
}
public static void main(String[] args) throws Exception {
publishMessagesIndividually();
publishMessagesInBatch();
handlePublishConfirmsAsynchronously();
}
static void publishMessagesIndividually() throws Exception {
try (Connection connection = createConnection()) {
Channel ch = connection.createChannel();
String queue = UUID.randomUUID().toString();
ch.queueDeclare(queue, false, false, true, null);
ch.confirmSelect();
long start = System.nanoTime();
for (int i = 0; i < MESSAGE_COUNT; i++) {
String body = String.valueOf(i);
ch.basicPublish("", queue, null, body.getBytes());
ch.waitForConfirmsOrDie(5_000);
}
long end = System.nanoTime();
System.out.format("Published %,d messages individually in %,d ms%n", MESSAGE_COUNT, Duration.ofNanos(end - start).toMillis());
}
}
static void publishMessagesInBatch() throws Exception {
try (Connection connection = createConnection()) {
Channel ch = connection.createChannel();
String queue = UUID.randomUUID().toString();
ch.queueDeclare(queue, false, false, true, null);
ch.confirmSelect();
int batchSize = 100;
int outstandingMessageCount = 0;
long start = System.nanoTime();
for (int i = 0; i < MESSAGE_COUNT; i++) {
String body = String.valueOf(i);
ch.basicPublish("", queue, null, body.getBytes());
outstandingMessageCount++;
if (outstandingMessageCount == batchSize) {
ch.waitForConfirmsOrDie(5_000);
outstandingMessageCount = 0;
}
}
if (outstandingMessageCount > 0) {
ch.waitForConfirmsOrDie(5_000);
}
long end = System.nanoTime();
System.out.format("Published %,d messages in batch in %,d ms%n", MESSAGE_COUNT, Duration.ofNanos(end - start).toMillis());
}
}
static void handlePublishConfirmsAsynchronously() throws Exception {
try (Connection connection = createConnection()) {
Channel ch = connection.createChannel();
String queue = UUID.randomUUID().toString();
ch.queueDeclare(queue, false, false, true, null);
ch.confirmSelect();
ConcurrentNavigableMap<Long, String> outstandingConfirms = new ConcurrentSkipListMap<>();
ConfirmCallback cleanOutstandingConfirms = (sequenceNumber, multiple) -> {
if (multiple) {
ConcurrentNavigableMap<Long, String> confirmed = outstandingConfirms.headMap(
sequenceNumber, true
);
confirmed.clear();
} else {
outstandingConfirms.remove(sequenceNumber);
}
};
ch.addConfirmListener(cleanOutstandingConfirms, (sequenceNumber, multiple) -> {
String body = outstandingConfirms.get(sequenceNumber);
System.err.format(
"Message with body %s has been nack-ed. Sequence number: %d, multiple: %b%n",
body, sequenceNumber, multiple
);
cleanOutstandingConfirms.handle(sequenceNumber, multiple);
});
long start = System.nanoTime();
for (int i = 0; i < MESSAGE_COUNT; i++) {
String body = String.valueOf(i);
outstandingConfirms.put(ch.getNextPublishSeqNo(), body);
ch.basicPublish("", queue, null, body.getBytes());
}
if (!waitUntil(Duration.ofSeconds(60), () -> outstandingConfirms.isEmpty())) {
throw new IllegalStateException("All messages could not be confirmed in 60 seconds");
}
long end = System.nanoTime();
System.out.format("Published %,d messages and handled confirms asynchronously in %,d ms%n", MESSAGE_COUNT, Duration.ofNanos(end - start).toMillis());
}
}
static boolean waitUntil(Duration timeout, BooleanSupplier condition) throws InterruptedException {
int waited = 0;
while (!condition.getAsBoolean() && waited < timeout.toMillis()) {
Thread.sleep(100L);
waited = +100;
}
return condition.getAsBoolean();
}
}
本文来自博客园,作者:衣来伸手饭来张口,转载请注明原文链接:https://www.cnblogs.com/autowin/p/15958655.html
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现