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消息传递模型的核心思想是生产者从不直接向队列发送任何消息。实际上,通常生产者甚至根本不知道消息是否会被发送到任何队列。

相反,生产者只能向交换器发送消息。交换是一件很简单的事情。一方面它接收来自生产者的消息,另一方面它将消息推送到队列中。交换器必须确切地知道如何处理它接收到的消息。它应该被添加到一个特定的队列中吗?它应该被追加到许多队列中吗?或者它应该被丢弃。这些规则由交换类型定义。

image-20220302150210521

有一些可用的交换类型: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)

https://www.rabbitmq.com/tutorials/tutorial-four-java.html

在前面的教程中,我们构建了一个简单的日志系统。我们能够将日志信息广播给许多接收者。

在本教程中,我们将为它添加一个特性——我们将使订阅消息的一个子集成为可能。例如,我们将能够只将关键错误消息指向日志文件(以节省磁盘空间),同时仍然能够在控制台上打印所有的日志消息。

绑定(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

https://www.rabbitmq.com/tutorials/tutorial-five-java.html

在之前的教程中,我们改进了日志系统。我们没有使用只能进行虚拟广播的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

https://www.rabbitmq.com/tutorials/tutorial-six-java.html

省略

7.发布者确认(Publisher Confirms)

https://www.rabbitmq.com/tutorials/tutorial-seven-java.html

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();
    }

}
posted @   衣来伸手饭来张口  阅读(53)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示