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 AMQPRabbitTemplate 封装了 Confirm 回调,配置 publisher-confirm-type: correlated 即可使用

相关推荐
2603_9547083115 小时前
全维度容错设计,打造微电网安全运行屏障
服务器·网络·数据库·人工智能·分布式·安全
国服第二切图仔16 小时前
HarmonyOS APP《画伴梦工厂》开发第60篇-分布式软总线2.0——多设备协同新范式
分布式·wpf·harmonyos
9523617 小时前
RabbitMQ-基础操作
java·spring boot·分布式·后端·spring·rabbitmq
worilb20 小时前
Spring Cloud 学习与实践(13):使用 Seata 解决分布式事务问题
分布式·学习·spring cloud
风若飞1 天前
Tomcat 9.0.118 分布式 Redis Session 配置指南
redis·分布式·tomcat
六bring个六1 天前
文件互传在传输层的操作分析
分布式·p2p·c/c++·open harmony
豆瓣鸡1 天前
Leaf 分布式 ID 生成实战——美团 Leaf 从原理到落地
java·spring boot·分布式
牛奶咖啡131 天前
大数据Hadoop运维应用实践——Hadoop大数据平台基础与架构(hadoop伪分布式的安装部署)
大数据·hadoop·分布式·hadoop是什么?有啥用?·hadoop的适用与不适用场景·hadoop伪分布式的安装部署·hadoop分布式存储操作