【RabbitMQ】发布确认模式(使用案例)

文章目录

  • [1. Publisher Confirms(发布确认)](#1. Publisher Confirms(发布确认))
  • [2. Publishing Messages Individually(单独确认)](#2. Publishing Messages Individually(单独确认))
  • [3. Publishing Messages in Batches(批量确认)](#3. Publishing Messages in Batches(批量确认))
  • [4. Handling Publisher Confirms Asynchronously(异步确认)](#4. Handling Publisher Confirms Asynchronously(异步确认))
  • [5. 三种策略完整代码](#5. 三种策略完整代码)

1. Publisher Confirms(发布确认)

作为消息中间件,都会面临消息丢失的问题。

消息丢失大概分为三种情况:

  • 1、生产者问题。因为应用程序故障,网络抖动等各种原因,生产者没有成功向 broker 发送消息。
  • 2、消息中间件自身问题。生产者成功发送给了 Broker,但是 Broker 没有把消息保存好,导致消息丢失。
  • 3、消费者问题。Broker 发送消息到消费者,消费者在消费消息时,因为没有处理好,导致 broker 将消费失败的消息从队列中删除了。

如下图所示:

RabbitMQ 也对上述问题给出了相应的解决方案。问题 2 可以通过持久化机制,问题 3 可以采用消息应答机制。针对问题1,可以采用发布确认(Publisher Confirms)机制实现。

发布确认属于 RabbitMQ 的七大工作模式之一,它是解决生产者的问题。

生产者将信道设置成 confirm(确认)模式,一旦信道进入 confirm 模式,所有在该信道上面发布的消息都会被指派一个唯一的 ID(从 1 开始),一旦消息被投递到所有匹配的队列之后,RabbitMQ 就会发送一个确认给生产者(包含消息的唯一 ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出。broker 回传给生产者的确认消息中 deliveryTag 包含了确认消息的序号,此外 broker 也可以设置 channel.basicAck 方法中的 multiple 参数,表示到这个序号之前的所有消息都已经得到了处理。

如下所示:

发送方确认机制最大的好处在于它是异步的,生产者可以同时发布消息和等待信道返回确认消息。

  • 1、当消息最终得到确认之后,生产者可以通过回调方法来处理该确认消息。
  • 2、如果 RabbitMQ 因为自身内部错误导致消息丢失,就会发送一条 nack(Basic.Nack)命令,生产者同样可以在回调方法中处理该 nack 命令。

使用发送确认机制,必须要信道设置成 confirm(确认)模式。

发布确认是 AMQP 0.9.1 协议的扩展,默认情况下它不会被启用。生产者通过 channel.confirmSelect() 将信道设置为 confirm 模式。

java 复制代码
Channel channel = connection.createChannel();
channel.confirmSelect();

发布确认有 3 种策略,接下来我们来学习这三种策略,每种策略都创建一个新的队列。

java 复制代码
// publisher confirms
public static final String PUBLISHER_CONFIRMS_QUEUE1 = "publisher.confirms.queue1";
public static final String PUBLISHER_CONFIRMS_QUEUE2 = "publisher.confirms.queue2";
public static final String PUBLISHER_CONFIRMS_QUEUE3 = "publisher.confirms.queue3";

2. Publishing Messages Individually(单独确认)

所谓单独确认,就是生产者每发送一条消息,就需要等待 Broker 的确认。

代码如下所示:

java 复制代码
/**
 * 单独确认
 */
private static void publishingMessagesIndividually() throws IOException, TimeoutException, InterruptedException {
    try(Connection connection = createConnection()) {
        // 1. 开启信道
        Channel channel = connection.createChannel();

        // 2. 设置信道为confirm模式
        channel.confirmSelect();

        // 3. 声明队列
        channel.queueDeclare(Constants.PUBLISHER_CONFIRMS_QUEUE1, true, false, false, null);

        // 4. 发生消息并等待确认
        long startTime = System.currentTimeMillis();
        for (int i = 0; i <MESSAGE_COUNT; i++) {
            // 发生消息
            String message = "Hello publisher confirms " + i;
            channel.basicPublish("", Constants.PUBLISHER_CONFIRMS_QUEUE1, null, message.getBytes());

            // 等待确认
            channel.waitForConfirmsOrDie(5000); // 等待5秒
        }
        long endTime = System.currentTimeMillis();
        System.out.printf("单独确认策略, 消息条数: %d, 耗时: %d ms\n", MESSAGE_COUNT, endTime - startTime);
    }
}

运行结果如下:

并且可以看到队列中也有 200 条消息:

消息内容如下:

但是可以发现,发送 200 条消息,耗时很长,用了 3518 ms。

观察上面代码,会发现这种策略是每发送一条消息后就调用 channel.waitForConfirmsOrDie 方法,之后等待服务端的确认,这实际上是一种串行同步等待的方式。尤其对于持久化的消息来说,需要等待消息确认存储在磁盘之后才会返回(调用 Linux 内核的 fsync 方法)。

但是发布确认机制是支持异步的,可以一边发送消息,一边等待消息确认。

由此进行了改进,接下来看另外一种策略:

  • Publishing Messages in Batches(批量确认):每发送一批消息后,调用 channel.waitForConfirms 方法,等待服务器的确认返回。

3. Publishing Messages in Batches(批量确认)

代码如下所示:

java 复制代码
/**
 * 批量确认
 */
private static void publishingMessagesInBatches() throws IOException, TimeoutException, InterruptedException {
    try(Connection connection = createConnection()) {
        // 1. 开启信道
        Channel channel = connection.createChannel();

        // 2. 设置信道为confirm模式
        channel.confirmSelect();

        // 3. 声明队列
        channel.queueDeclare(Constants.PUBLISHER_CONFIRMS_QUEUE2, true, false, false, null);

        // 4. 发生消息, 并进行确认
        long startTime = System.currentTimeMillis();
        int batchSize = 100; // 设置批量处理的大小(每100条就确认一次)
        int  outStandingMessageCount = 0; // 计数器
        for (int i = 0; i <MESSAGE_COUNT; i++) {
            // 发生消息
            String message = "Hello publisher confirms " + i;
            channel.basicPublish("", Constants.PUBLISHER_CONFIRMS_QUEUE2, null, message.getBytes());
            outStandingMessageCount++;
            if (outStandingMessageCount == batchSize) {
                // 确认
                channel.waitForConfirmsOrDie(5000); // 等待5秒
                outStandingMessageCount = 0; // 重置计数器
            }
        }
        if(outStandingMessageCount > 0) {
            channel.waitForConfirmsOrDie(5000);
        }
        long endTime = System.currentTimeMillis();
        System.out.printf("批量确认策略, 消息条数: %d, 耗时: %d ms\n", MESSAGE_COUNT, endTime - startTime);
    }
}

运行结果如下:

可以观察到,性能提高了很多。

相比于单独确认策略,批量确认极大地提升了 confirm 的效率,缺点是出现 Basic.Nack 或者超时时,我们不清楚具体哪条消息出了问题。客户端需要将这一批次的消息全部重发,这会带来明显的重复消息数量。

当消息经常丢失时,批量确认的性能应该是不升反降的。

由此进行了改进,接下来看最后一种策略:

  • Handling Publisher Confirms Asynchronously(异步确认):提供一个回调方法,服务端确认了一条或者多条消息后客户端会回这个方法进行处理。

4. Handling Publisher Confirms Asynchronously(异步确认)

异步 confirm 方法的编程实现最为复杂。Channel 接口提供了一个方法 addConfirmListener,这个方法可以添加 ConfirmListener 回调接口。

ConfirmListener 接口中包含两个方法:handleAck(long deliveryTag, boolean multiple)handleNack(long deliveryTag, boolean multiple),分别对应处理 RabbitMQ 发送给生产者的 ack 和 nack。

deliveryTag 表示发送消息的序号。multiple 表示是否批量确认。

我们需要为每一个 Channel 维护一个已发送消息的序号集合。当收到 RabbitMQ 的 confirm 回调时,从集合中删除对应的消息。当 Channel 开启 confirm 模式后,channel 上发送消息都会附带一个从 1 开始递增的 deliveryTag 序号。我们可以使用 SortedSet 的有序性来维护这个已发消息的集合。

  • 1、当收到 ack 时,从序列中删除该消息的序号。如果为批量确认消息,表示小于等于当前序号 deliveryTag 的消息都收到了,则清除对应集合。
  • 2、当收到 nack 时,处理逻辑类似,不过需要结合具体的业务情况,进行消息重发等操作。

代码如下所示:

java 复制代码
/**
 * 异步确认
 */
private static void handlingPublisherConfirmsAsynchronously() throws IOException, TimeoutException, InterruptedException {
    try(Connection connection = createConnection()) {
        // 1. 开启信道
        Channel channel = connection.createChannel();

        // 2. 设置信道为confirm模式
        channel.confirmSelect();

        // 3. 声明队列
        channel.queueDeclare(Constants.PUBLISHER_CONFIRMS_QUEUE3, true, false, false, null);

        // 4. 监听confirm
        long startTime = System.currentTimeMillis();
        // 有序集合, 元素按照⾃然顺序进⾏排序, 集合中存储的是未确认的消息ID
        SortedSet<Long> confirmSeqNo = Collections.synchronizedSortedSet(new TreeSet<>());

        channel.addConfirmListener(new ConfirmListener() {
            @Override
            public void handleAck(long deliveryTag, boolean multiple) throws IOException {
                if (multiple) {
                    // 批量确认:将集合中⼩于等于当前序号deliveryTag元素的集合清除,表⽰这批序号的消息都已经被ack了
                    confirmSeqNo.headSet(deliveryTag + 1).clear();
                }
                else {
                    //单条确认:将当前的deliveryTag从集合中移除
                    confirmSeqNo.remove(deliveryTag);
                }
            }

            @Override
            public void handleNack(long deliveryTag, boolean multiple) throws IOException {
                if (multiple) {
                    // 批量确认:将集合中⼩于等于当前序号deliveryTag元素的集合清除,表⽰这批序号的消息都已经被ack了
                    confirmSeqNo.headSet(deliveryTag + 1).clear();
                }
                else {
                    //单条确认:将当前的deliveryTag从集合中移除
                    confirmSeqNo.remove(deliveryTag);
                }
                //如果处理失败,这⾥需要添加处理消息重发的场景, 此处代码省略
                //TODO......
            }
        });

        // 5. 发生消息
        // 循环发送消息
        for (int i = 0; i <MESSAGE_COUNT; i++) {
            String message = "Hello publisher confirms " + i;
            // 得到下次发送消息的序号, 从1开始
            long seqNo = channel.getNextPublishSeqNo();
            channel.basicPublish("", Constants.PUBLISHER_CONFIRMS_QUEUE3, null, message.getBytes());

            // 将序号存⼊集合中
            confirmSeqNo.add(seqNo);
        }
        // 消息确认完毕
        while (!confirmSeqNo.isEmpty()) {
            Thread.sleep(100); // 休眠100毫秒
        }
        long endTime = System.currentTimeMillis();
        System.out.printf("异步确认策略, 消息条数: %d, 耗时: %d ms\n", MESSAGE_COUNT, endTime - startTime);
    }
}

运行结果如下:把消息条数改为 10000 条,只对比批量确认即可。

可以看到,异步确认策略下,消息量越大,那么性能就越好。

5. 三种策略完整代码

代码如下所示:

java 复制代码
package publisher.confirms;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.ConfirmListener;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import constant.Constants;

import java.io.IOException;
import java.util.Collections;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.TimeoutException;

public class PublisherConfirms {
    private static final Integer MESSAGE_COUNT = 20000; // 测试的消息数

    static Connection createConnection() throws IOException, TimeoutException {
        // 建立连接
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost(Constants.HOST);   // MQ所在的服务器地址
        factory.setPort(Constants.PORT);            // 端口号
        factory.setUsername(Constants.USERNAME);    // 账号
        factory.setPassword(Constants.PASSWORD);    // 密码
        factory.setVirtualHost(Constants.VIRTUAL_HOST);      // 虚拟主机
        return factory.newConnection();
    }

    public static void main(String[] args) throws IOException, InterruptedException, TimeoutException {
        // 策略一:单独确认
        publishingMessagesIndividually();

        // 策略二:批量确认
        publishingMessagesInBatches();

        // 策略一:异步确认
        handlingPublisherConfirmsAsynchronously();
    }

    /**
     * 异步确认
     */
    private static void handlingPublisherConfirmsAsynchronously() throws IOException, TimeoutException, InterruptedException {
        try(Connection connection = createConnection()) {
            // 1. 开启信道
            Channel channel = connection.createChannel();

            // 2. 设置信道为confirm模式
            channel.confirmSelect();

            // 3. 声明队列
            channel.queueDeclare(Constants.PUBLISHER_CONFIRMS_QUEUE3, true, false, false, null);

            // 4. 监听confirm
            long startTime = System.currentTimeMillis();
            // 有序集合, 元素按照⾃然顺序进⾏排序, 集合中存储的是未确认的消息ID
            SortedSet<Long> confirmSeqNo = Collections.synchronizedSortedSet(new TreeSet<>());

            channel.addConfirmListener(new ConfirmListener() {
                @Override
                public void handleAck(long deliveryTag, boolean multiple) throws IOException {
                    if (multiple) {
                        // 批量确认:将集合中⼩于等于当前序号deliveryTag元素的集合清除,表⽰这批序号的消息都已经被ack了
                        confirmSeqNo.headSet(deliveryTag + 1).clear();
                    }
                    else {
                        //单条确认:将当前的deliveryTag从集合中移除
                        confirmSeqNo.remove(deliveryTag);
                    }
                }

                @Override
                public void handleNack(long deliveryTag, boolean multiple) throws IOException {
                    if (multiple) {
                        // 批量确认:将集合中⼩于等于当前序号deliveryTag元素的集合清除,表⽰这批序号的消息都已经被ack了
                        confirmSeqNo.headSet(deliveryTag + 1).clear();
                    }
                    else {
                        //单条确认:将当前的deliveryTag从集合中移除
                        confirmSeqNo.remove(deliveryTag);
                    }
                    //如果处理失败,这⾥需要添加处理消息重发的场景, 此处代码省略
                    //TODO......
                }
            });

            // 5. 发生消息
            // 循环发送消息
            for (int i = 0; i <MESSAGE_COUNT; i++) {
                String message = "Hello publisher confirms " + i;
                // 得到下次发送消息的序号, 从1开始
                long seqNo = channel.getNextPublishSeqNo();
                channel.basicPublish("", Constants.PUBLISHER_CONFIRMS_QUEUE3, null, message.getBytes());

                // 将序号存⼊集合中
                confirmSeqNo.add(seqNo);
            }
            // 消息确认完毕
            while (!confirmSeqNo.isEmpty()) {
                Thread.sleep(100); // 休眠100毫秒
            }
            long endTime = System.currentTimeMillis();
            System.out.printf("异步确认策略, 消息条数: %d, 耗时: %d ms\n", MESSAGE_COUNT, endTime - startTime);
        }
    }

    /**
     * 批量确认
     */
    private static void publishingMessagesInBatches() throws IOException, TimeoutException, InterruptedException {
        try(Connection connection = createConnection()) {
            // 1. 开启信道
            Channel channel = connection.createChannel();

            // 2. 设置信道为confirm模式
            channel.confirmSelect();

            // 3. 声明队列
            channel.queueDeclare(Constants.PUBLISHER_CONFIRMS_QUEUE2, true, false, false, null);

            // 4. 发生消息, 并进行确认
            long startTime = System.currentTimeMillis();
            int batchSize = 100; // 设置批量处理的大小(每100条就确认一次)
            int  outStandingMessageCount = 0; // 计数器
            for (int i = 0; i <MESSAGE_COUNT; i++) {
                // 发生消息
                String message = "Hello publisher confirms " + i;
                channel.basicPublish("", Constants.PUBLISHER_CONFIRMS_QUEUE2, null, message.getBytes());
                outStandingMessageCount++;
                // 批量确认消息
                if (outStandingMessageCount == batchSize) {
                    channel.waitForConfirmsOrDie(5000); // 等待5秒
                    outStandingMessageCount = 0; // 重置计数器
                }
            }
            // 消息发送完, 还有未确认的消息, 进行确认
            if(outStandingMessageCount > 0) {
                channel.waitForConfirmsOrDie(5000);
            }
            long endTime = System.currentTimeMillis();
            System.out.printf("批量确认策略, 消息条数: %d, 耗时: %d ms\n", MESSAGE_COUNT, endTime - startTime);
        }
    }

    /**
     * 单独确认
     */
    private static void publishingMessagesIndividually() throws IOException, TimeoutException, InterruptedException {
        try(Connection connection = createConnection()) {
            // 1. 开启信道
            Channel channel = connection.createChannel();

            // 2. 设置信道为confirm模式
            channel.confirmSelect();

            // 3. 声明队列
            channel.queueDeclare(Constants.PUBLISHER_CONFIRMS_QUEUE1, true, false, false, null);

            // 4. 发生消息, 等待确认
            long startTime = System.currentTimeMillis();
            for (int i = 0; i <MESSAGE_COUNT; i++) {
                // 发生消息
                String message = "Hello publisher confirms " + i;
                channel.basicPublish("", Constants.PUBLISHER_CONFIRMS_QUEUE1, null, message.getBytes());

                // 等待确认
                channel.waitForConfirmsOrDie(5000); // 等待5秒
            }
            long endTime = System.currentTimeMillis();
            System.out.printf("单独确认策略, 消息条数: %d, 耗时: %d ms\n", MESSAGE_COUNT, endTime - startTime);
        }
    }
}

把消息条数改为 20000 条,运行结果如下:

可以看到,消息数越多,异步确认的优势越明显。

相关推荐
EXnf1SbYK3 小时前
Redis分布式锁进阶第十二篇:全系列终极兜底复盘 + 锁架构巡检落地 + 线上零事故收尾方案
redis·分布式·架构
EXnf1SbYK3 小时前
Redis分布式锁进阶第八篇:锁超时乱序深度踩坑 + 看门狗失效真实溯源 + 业务长耗时标准化兜底方案
数据库·redis·分布式
EXnf1SbYK3 小时前
Redis分布式锁进阶第十一篇
数据库·redis·分布式
biyezuopinvip4 小时前
分布式风电场低电压穿越故障建模与仿真
分布式·matlab·毕业设计·毕业论文·分布式风电场·低电压穿越故障·建模与仿真
苍煜4 小时前
SpringBoot单体应用到分布式下的数据库锁、事务、Redis事务、分布式锁、分布式事务协调
数据库·spring boot·分布式
fengxin_rou4 小时前
黑马点评项目万字总结:从redis基础到实战应用详解
java·开发语言·分布式·后端·黑马点评
小江的记录本15 小时前
【Kafka核心】架构模型:Producer、Broker、Consumer、Consumer Group、Topic、Partition、Replica
java·数据库·分布式·后端·搜索引擎·架构·kafka
身如柳絮随风扬1 天前
多数据源切换实战:从业务场景到3种实现方案全解析
java·分布式·微服务
AIMath~1 天前
雪花算法+ZooKeeper解决方案+RPC是什么
分布式·zookeeper·云原生