文章目录
- [RabbitMQ 发布确认模式(Publisher Confirms)](#RabbitMQ 发布确认模式(Publisher Confirms))
-
- 一、为什么需要发布确认?
- 二、开启确认模式
- 三、三种确认策略
-
- [3.1 单条同步确认](#3.1 单条同步确认)
- [3.2 批量同步确认(推荐入门)](#3.2 批量同步确认(推荐入门))
- [3.3 异步回调确认(生产推荐)](#3.3 异步回调确认(生产推荐))
- 四、关键细节与常见坑
-
- [4.1 `deliveryTag` 在 Confirm 模式下是发布序号](#4.1
deliveryTag在 Confirm 模式下是发布序号) - [4.2 序号必须在发送前获取](#4.2 序号必须在发送前获取)
- [4.3 `multiple=true` 时的清理逻辑](#4.3
multiple=true时的清理逻辑) - [4.4 nack 的常见原因](#4.4 nack 的常见原因)
- [4.5 同步 vs 异步的选择](#4.5 同步 vs 异步的选择)
- [4.1 `deliveryTag` 在 Confirm 模式下是发布序号](#4.1
- 五、三种模式对比总结
- 六、延伸阅读
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 异步回调确认(生产推荐)
发送与确认解耦,吞吐量最高。核心思路:
- 发送前用
getNextPublishSeqNo()取序号 - 用
SortedSet跟踪未确认序号 - 用
Map<Long, byte[]>保存消息体,供 nack 时重发 - 注册
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即可使用