RabbitMQ Publisher_Confirms(发布确认)

文章目录

  • [RabbitMQ 发布确认模式(Publisher Confirms)](#RabbitMQ 发布确认模式(Publisher Confirms))

RabbitMQ 发布确认模式(Publisher Confirms)

一、为什么需要发布确认?

在简单工作队列中,生产者把消息 basicPublish 出去就认为发送成功。但这条消息可能在中途丢失:

  • 网络抖动,Broker 没收到
  • 交换机不存在,消息被丢弃
  • 队列满了,消息被拒绝

发布确认(Publisher Confirms) 是 AMQP 0-9-1 的扩展机制,让 Broker 主动告诉生产者:"这条消息我已经收到了"。思路类似 TCP 的 ACK,但默认不开启 ,需要手动调用 channel.confirmSelect()。

注意区分两个"确认"

机制 确认方 含义
Publisher Confirm Broker → 生产者 消息已被 Broker 接收(入队或路由成功)
Consumer Ack 消费者 → Broker 消息已被消费者 处理完成

发布确认不能保证消费者一定收到或处理成功,那是消费者 ACK 的职责。

在资金流水、库存同步等对可靠性要求高的场景,发布确认是生产端的标配。


二、开启确认模式

无论采用哪种确认策略,第一步都一样:

java 复制代码
channel.confirmSelect(); // 将 Channel 设为 confirm 模式

此后每次 basicPublish,Broker 都会返回 ack(成功)或 nack(失败)。


三、三种确认策略

策略 方式 适用场景 吞吐量
单条同步 发一条 → waitForConfirms() 消息极少、要求逐条可控 最低
批量同步 发 N 条 → waitForConfirmsOrDie() 大批量、可容忍批量失败 中等
异步回调 ConfirmListener 边发边收 高吞吐、需细粒度重发 最高

下面分别给出完整示例。连接参数统一使用常量类管理(推荐做法):

java 复制代码
// constants/RabbitConstant.java
public final class RabbitConstant {
    public static final String HOST = "你的服务器IP";
    public static final int PORT = 5672;
    public static final String USERNAME = "study";
    public static final String PASSWORD = "study";
    public static final String VIRTUAL_HOST = "test-virtual";
    public static final String QUEUE_HELLO = "hello";
}

Maven 依赖:

xml 复制代码
<dependency>
    <groupId>com.rabbitmq</groupId>
    <artifactId>amqp-client</artifactId>
    <version>5.25.0</version>
</dependency>

3.1 单条同步确认

每发一条就阻塞等待 Broker 回复,最简单也最慢:

java 复制代码
channel.confirmSelect();

for (int i = 0; i < 10; i++) {
    String body = "消息_" + i;
    channel.basicPublish("", RabbitConstant.QUEUE_HELLO, null, body.getBytes());

    // 等待当前这条消息的确认,超时 5 秒抛 IOException
    channel.waitForConfirmsOrDie(5_000);
    System.out.println("消息 " + i + " 确认成功");
}

waitForConfirmsOrDie() 的行为:

  • 全部 ack → 正常返回
  • 收到 nack → 抛 IOException: "n messages not confirmed"
  • 超时未收到回复 → 抛 IOException: "timed out waiting for confirms"

3.2 批量同步确认(推荐入门)

攒够一批再统一等待,大幅减少网络往返:

java 复制代码
ConnectionFactory factory = new ConnectionFactory();
factory.setHost(RabbitConstant.HOST);
factory.setPort(RabbitConstant.PORT);
factory.setUsername(RabbitConstant.USERNAME);
factory.setPassword(RabbitConstant.PASSWORD);
factory.setVirtualHost(RabbitConstant.VIRTUAL_HOST);

Connection connection = factory.newConnection();
Channel channel = connection.createChannel();

channel.queueDeclare(RabbitConstant.QUEUE_HELLO, true, false, false, null);
channel.confirmSelect(); // ← 原文遗漏了这一步,不调用则不会收到确认

int batchSize = 100;              // 每批条数
int outstandingCount = 0;         // 当前批次已发未确认条数

for (int i = 0; i < 560; i++) {
    String body = "批量消息_" + i;
    channel.basicPublish("", RabbitConstant.QUEUE_HELLO, null, body.getBytes());
    outstandingCount++;

    if (outstandingCount == batchSize) {
        channel.waitForConfirmsOrDie(5_000); // 等待本批全部确认
        System.out.println("【批量确认】" + batchSize + " 条消息确认成功");
        outstandingCount = 0;
    }
}

// 处理最后不足一批的剩余消息
if (outstandingCount > 0) {
    channel.waitForConfirmsOrDie(5_000);
    System.out.println("【批量确认】剩余 " + outstandingCount + " 条消息确认成功");
}

channel.close();
connection.close();

multiple 参数的含义(同步模式下体现在 Broker 侧):

Broker 可以对一批消息回复一条 ack,deliveryTag 表示"此序号及之前的消息全部确认"。同步的 waitForConfirms() 会在内部处理这些批量 ack。


3.3 异步回调确认(生产推荐)

发送与确认解耦,吞吐量最高。核心思路:

  1. 发送前用 getNextPublishSeqNo() 取序号
  2. 用 SortedSet 跟踪未确认序号
  3. 用 Map<Long, byte[]> 保存消息体,供 nack 时重发
  4. 注册 ConfirmListener 处理 ack / nack
java 复制代码
channel.confirmSelect();

// 未确认序号集合(线程安全)
SortedSet<Long> unconfirmedSet = Collections.synchronizedSortedSet(new TreeSet<>());
// 序号 → 消息体,用于 nack 重发
Map<Long, byte[]> messageStore = new ConcurrentHashMap<>();

channel.addConfirmListener(new ConfirmListener() {
    @Override
    public void handleAck(long deliveryTag, boolean multiple) {
        System.out.println("【ACK】序号=" + deliveryTag + ", 批量=" + multiple);
        if (multiple) {
            // 批量确认:deliveryTag 及之前的所有序号均已确认
            unconfirmedSet.headSet(deliveryTag + 1).clear();
            messageStore.keySet().removeIf(tag -> tag <= deliveryTag);
        } else {
            unconfirmedSet.remove(deliveryTag);
            messageStore.remove(deliveryTag);
        }
    }

    @Override
    public void handleNack(long deliveryTag, boolean multiple) {
        System.out.println("【NACK】序号=" + deliveryTag + ", 批量=" + multiple);

        // 收集需要重发的序号
        SortedSet<Long> failedTags = multiple
            ? new TreeSet<>(unconfirmedSet.headSet(deliveryTag + 1))
            : new TreeSet<>(Collections.singleton(deliveryTag));

        // 从跟踪集合中移除
        if (multiple) {
            unconfirmedSet.headSet(deliveryTag + 1).clear();
        } else {
            unconfirmedSet.remove(deliveryTag);
        }

        // 重发失败消息
        for (Long tag : failedTags) {
            byte[] body = messageStore.remove(tag);
            if (body != null) {
                long newSeqNo = channel.getNextPublishSeqNo(); // 先取序号
                channel.basicPublish("", RabbitConstant.QUEUE_HELLO, null, body);
                unconfirmedSet.add(newSeqNo);
                messageStore.put(newSeqNo, body);
                System.out.println("【重发】原序号=" + tag + " → 新序号=" + newSeqNo);
            }
        }
    }
});

// 发送消息
for (int i = 0; i < 10; i++) {
    byte[] bodyBytes = ("消息_" + i).getBytes();
    long seqNo = channel.getNextPublishSeqNo();  // ← 必须在 basicPublish 之前调用
    channel.basicPublish("", RabbitConstant.QUEUE_HELLO, null, bodyBytes);
    unconfirmedSet.add(seqNo);
    messageStore.put(seqNo, bodyBytes);
}

// 等待所有消息确认完毕
while (!unconfirmedSet.isEmpty()) {
    Thread.sleep(10);
}
System.out.println("所有消息确认完毕");

四、关键细节与常见坑

4.1 deliveryTag 在 Confirm 模式下是发布序号

在 ConfirmListener 回调里,deliveryTag 不是 消费者那边的投递标签,而是 publish sequence number (从 1 开始递增)。不要和消费者 Envelope.getDeliveryTag() 混用。

4.2 序号必须在发送前获取

java 复制代码
// ✅ 正确
long seqNo = channel.getNextPublishSeqNo();
channel.basicPublish(...);

// ❌ 错误:先发送再取序号,序号会错位
channel.basicPublish(...);
long seqNo = channel.getNextPublishSeqNo();

4.3 multiple=true 时的清理逻辑

复制代码
Broker 回复: ack(deliveryTag=5, multiple=true)
含义: 序号 1、2、3、4、5 全部确认成功

清理方式: unconfirmedSet.headSet(5 + 1).clear()
         即删除所有 < 6 的序号

4.4 nack 的常见原因

原因 说明
交换机不存在 消息无法路由
队列达到 x-max-length 队列满,消息被拒绝
集群脑裂后恢复 极少数情况下历史消息被 nack

nack 不会抛异常 ,必须在 handleNack 里自行实现重发或告警。

4.5 同步 vs 异步的选择

消息量 推荐模式
< 100/秒 单条同步,代码最简单
100~10000/秒 批量同步,batchSize 建议 100~500
> 10000/秒 异步回调,配合消息存储 + 重发

五、三种模式对比总结

模式 核心 API 失败处理 吞吐量
单条同步 waitForConfirmsOrDie 抛 IOException ★☆☆☆☆
批量同步 waitForConfirmsOrDie 抛 IOException ★★★☆☆
异步回调 ConfirmListener handleNack 重发 ★★★★★

六、延伸阅读

  • Mandatory 标志 + ReturnListener:消息无法路由到任何队列时,Broker 会将消息退回生产者(与 Confirm 互补)
  • 事务模式(tx):也能保证可靠性,但性能约为 Confirm 的 1/100,生产环境不推荐
  • Spring AMQP :RabbitTemplate 封装了 Confirm 回调,配置 publisher-confirm-type: correlated 即可使用

相关推荐
需要82644 分钟前
分布式 ID 生成:雪花算法、号段模式与时钟回拨
分布式·算法
灯澜忆梦5 小时前
【RabbitMQ #11】 | 消费者可靠性
分布式·rabbitmq·ruby
灯澜忆梦6 小时前
【RabbitMQ #9】 | 生产者可靠性
分布式·rabbitmq
zhangzeyuaaa7 小时前
深入理解 Ruby 可变对象与不可变对象的原理、坑点与最佳实践
开发语言·后端·ruby
银河技术8 小时前
TB级文本去重实战:从单机 OOM 到 Spark / Ray 分布式架构的工程演进
分布式·微服务·重构·架构·spark·llm·rag
deepdata_cn9 小时前
从集中式到分布式:Data Mesh颠覆传统数据架构的底层逻辑
分布式·data mesh
5008410 小时前
React Native for OpenHarmony 实战:三方库 react-native-device-uptime 的鸿蒙化适配指南
javascript·分布式·react native·react.js·harmonyos
灯澜忆梦17 小时前
【RabbitMQ #10】 | MQ可靠性
分布式·rabbitmq
醉颜凉19 小时前
Kafka 与 RabbitMQ/RocketMQ 选型对比:场景匹配、性能基准与迁移成本
kafka·消息队列·rabbitmq·rocketmq·中间件选型
逐流人21 小时前
Ceph分布式存储集群配置与池管理:从配置优先级到PG、复本池与纠删码池
运维·分布式·ceph·云原生·云计算·rados