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 即可使用

相关推荐
江畔柳前堤12 小时前
云原生 × AI 全景图谱:从 Kubernetes 到 Agent 基础设施的一次认知跃迁
人工智能·分布式·gpt·云原生·kubernetes·原型模式·agi
Thomas214316 小时前
kafka 存储flink changelog两种方式和形态
分布式·flink·kafka
GIR12316 小时前
首发:2026年分布式储能系统市场分析报告——覆盖发展前景、占有率与十五五规划
分布式
leo_messi9416 小时前
面试知识点梳理及相关面试题(十六)-- 分布式设计
分布式·面试·职场和发展
IT大白鼠1 天前
MySQL 分布式集群系列 · 第四篇——实操部署指南:从零搭建生产级 MySQL NDB 集群
数据库·分布式·mysql
IT大白鼠1 天前
MySQL 分布式集群系列 · 第三篇——核心原理精讲:NDB 自动分片、多主写入与数据同步
数据库·分布式·mysql
星云API技术支持1 天前
企业微信二次开发:如何建立统一的错误处理、日志与请求追踪机制
javascript·企业微信·ruby
Cry丶2 天前
第四篇:Block、Proc、Lambda 与 `yield`
ruby·lambda·block·yield·闭包·proc
星恒讯工业路由器2 天前
分布式光伏站点分散难联网?4G/5G点对多点自组网方案解析
分布式·物联网·5g·分布式光伏·自组网·点对多点通信·4g/5g
qq_452396232 天前
第十一篇:《eBPF 进阶:自定义探针与生产级可观测性部署》
分布式