RabbitMQ非官方教程(八)消息确认

消息确认是实现可靠的RabbitMQ扩展。在通道上启用发布者确认后,代理将异步确认客户端发布的消息,这意味着它们已在服务器端处理。在节教程将使用发布者确认来确保发布的消息已安全到达代理。我们将介绍几种使用发布者确认并解释其优缺点的策略。

在频道上启用发布者确认

发布者确认是AMQP 0.9.1协议的RabbitMQ扩展,因此默认情况下未启用它们。发布者确认在通道级别使用confirmSelect方法启用:

Channel channel = connection.createChannel();
channel.confirmSelect();

必须在希望使用发布者确认的每个频道上调用此方法。确认仅应启用一次,而不是对每个已发布的消息都启用。

策略1:分别发布消息

让我们从使用确认发布的最简单方法开始,即发布消息并同步等待其确认:

while (thereAreMessagesToPublish()) {
    byte[] body = ...;
    BasicProperties properties = ...;
    channel.basicPublish(exchange, queue, properties, body);
    // uses a 5 second timeout
    channel.waitForConfirmsOrDie(5_000);
}

在前面的示例中,我们像往常一样发布一条消息,并等待通过Channel#waitForConfirmsOrDie(long)方法对其进行确认。确认消息后,该方法立即返回。如果未在超时时间内确认该消息或该消息没有被确认(这意味着代理出于某种原因无法处理该消息),则该方法将引发异常。异常的处理通常包括记录错误消息和/或重试发送消息。

不同的客户端库有不同的方式来同步处理发布者的确认,因此请确保仔细阅读所使用客户端的文档。该技术非常简单,但也有一个主要缺点:由于消息的确认会阻止所有后续消息的发布,因此它会大大减慢发布速度。这种方法不会提供每秒超过数百条已发布消息的吞吐量。但是,对于某些应用程序来说这可能已经足够了。

发布者确认异步吗?

我们在一开始提到代理是异步地确认发布的消息,但是在第一个示例中,代码同步等待直到消息被确认。客户端实际上异步接收确认,并相应地取消阻止对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:处理发布者异步确认

代理异步确认已发布的消息,只需在客户端上注册一个回调即可收到这些确认的通知:

Channel channel = connection.createChannel();
channel.confirmSelect();
channel.addConfirmListener((sequenceNumber, multiple) -> {
    // 确认消息时的代码
}, (sequenceNumber, multiple) -> {
    // 消息未确认时的代码
});

有两种回调:一种用于确认消息,另一种用于小消息(代理可以认为丢失的消息)。每个回调都有2个参数:

  • 序列号:标识已确认或未确认消息的数字。我们很快将看到如何将其与已发布的消息相关联。
  • 整数:这是一个布尔值。如果为false,则仅确认/取消一条消息;如果为true,则将确认/不添加序列号较低或相等的所有消息。

可以在发布之前使用Channel#getNextPublishSeqNo()获得序列号:

int sequenceNumber = channel.getNextPublishSeqNo());
ch.basicPublish(exchange, queue, properties, body);

将消息与序列号关联的一种简单方法是使用映射。假设我们要发布字符串,因为它们很容易变成要发布的字节数组。这是一个使用映射将发布序列号与消息的字符串主体相关联的代码示例:

ConcurrentNavigableMap<Long, String> outstandingConfirms = new ConcurrentSkipListMap<>();
// 确认回调的代码将在以后出现
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);
});

前一个示例包含一个回调函数,它在确认到达时清除映射。注意,这个回调同时处理单次和多次确认。当确认到达时使用这个回调函数(作为Channel#addConfirmListener的第一个参数)。缺少消息的回调函数检索消息体并发出警告。然后它重用前面的回调函数来清理未完成确认的映射(无论消息是已确认的还是没有确定的,都必须删除映射中相应的条目)。

如何跟踪未完成的确认?

我们的示例使用ConcurrentNavigableMap来跟踪未完成的确认。由于以下几个原因,此数据结构很方便。它允许轻松地将序列号与消息相关联(无论消息数据是什么),还可以轻松清除条目直到给定序列ID(以处理多个确认/提示)。最后,它支持并发访问,因为在客户端库拥有的线程中调用了确认回调,该线程应与发布线程保持不同。

除了使用复杂的映射实现之外,还有其他跟踪未完成确认的方法,例如使用简单的并发哈希映射和变量来跟踪发布序列的下限,但是它们通常更复杂。

综上所述,处理发布者异步确认通常需要执行以下步骤:

  • 提供一种将发布序列号与消息相关联的方法。
  • 在通道上注册一个确认侦听器,以便在发布者确认/通知到达后执行相应操作(例如记录或重新发布未确认的消息)时收到通知。序列号与消息的关联机制在此步骤中可能还需要进行一些清洗。
  • 在发布消息之前跟踪发布序列号。

重新发布nack-ed消息?

从相应的回调中重新发布一个nack-ed消息可能很诱人,但是应该避免这种情况,因为确认回调是在不应执行通道的I / O线程中分派的。更好的解决方案是将消息放入由发布线程轮询的内存队列中。诸如ConcurrentLinkedQueue之类的是在确认回调和发布线程之间传输消息的理想选择。

在某些应用程序中,确保将发布的消息发送到代理非常重要。发布者确认是RabbitMQ功能,可以帮助满足此要求。发布者确认本质上是异步的,但也可以同步处理它们。没有确定的方法可以实现发布者确认,这通常归结为应用程序和整个系统中的约束。典型的技术有:

  • 分别发布消息,同步等待确认:简单,但吞吐量非常有限。
  • 批量发布消息,同步等待批量确认:简单,合理的吞吐量,但是很难推断出什么时候出了问题。
  • 异步处理:最佳性能和资源使用,在出现错误的情况下可以很好地控制,但可以正确实施。

现在把上面所有的代码整合在一起,

package com.mytest.rabbitMQ.Seventh;

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

}

如果客户端和服务器位于同一台计算机上,则计算机上的输出应看起来相似。单独发布消息的效果不理想,批处理发布和异步处理结果好一点。

Publisher确认非常依赖于网络,我们可以尝试使用远程节点,因为客户端和服务器通常不在生产中的同一台计算机上,所以这是更现实的选择。 PublisherConfirms.java可以轻松更改为使用非本地节点:

static Connection createConnection() throws Exception {
    ConnectionFactory cf = new ConnectionFactory();
    cf.setHost("host");
    cf.setUsername("userName");
    cf.setPassword("password");
    return cf.newConnection();
}

批量发布很容易实现,但是在发布者不确认的情况下,不容易知道哪些消息无法发送给代理。处理发布者确认异步涉及更多的实现,但是提供更好的粒度和更好地控制在处理发布的消息时执行的操作。

这是本节的demo代码地址:https://gitee.com/mjTree/javaDevelop/tree/master/testDemo

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值