前言
在 RabbitMQ 中,消息投递成功不等于业务处理成功。
生产者把消息投递到 RabbitMQ,只解决了「消息进入 Broker」的问题;RabbitMQ 把消息推给消费者,也只解决了「消息到达消费者进程」的问题。真正困难的是:
消费者拿到消息后,业务逻辑执行到一半宕机怎么办?
消费者抛异常了,消息是丢弃、重试,还是进入死信队列?
控制台里的Ready和Unacked到底在表达什么?
这就是 Message Acknowledgement,消息确认机制要解决的问题。
RabbitMQ 官方文档也明确区分了两类确认机制:
| 方向 | 机制 | 作用 |
|---|---|---|
| Producer -> RabbitMQ | Publisher Confirms | 确认消息是否成功到达 Broker |
| RabbitMQ -> Consumer | Consumer Acknowledgements | 确认消息是否被消费者成功处理 |
本文只聚焦后者:消费端确认机制。
一、核心机制解析:自动确认与手动确认
RabbitMQ 在消费者订阅队列时,会通过 autoAck 决定确认模式。
channel.basicConsume(queueName, autoAck, consumer);
| 模式 | autoAck | RabbitMQ 何时删除消息 | 可靠性 | 适用场景 |
|---|---|---|---|---|
| 自动确认 | true | 消息一投递给消费者就视为成功 | 低 | 日志、指标、允许少量丢失的异步任务 |
| 手动确认 | false | 消费者显式调用 basicAck 后删除 | 高 | 订单、支付、库存、积分、通知等核心链路 |
自动确认本质上是 fire-and-forget。
RabbitMQ 只负责把消息发出去,不关心消费者有没有真正处理完成。
1. 自动确认:吞吐高,但容易丢消息
自动确认流程:
Producer -> Exchange -> Queue -> Consumer
|
| autoAck=true
v
RabbitMQ 立即删除消息
如果消费者收到消息后立刻宕机:
- RabbitMQ 已经认为消息成功;
- 队列中不会再保留该消息;
- 消息直接丢失。
适合:
- 可丢失的监控埋点;
- 非核心日志;
- 消费端处理极快且幂等成本低的场景。
不适合:
- 金额变更;
- 库存扣减;
- 订单状态流转;
- 任何「丢一条就要查事故」的系统。
1.1 自动确认代码实现
配置交换机和队列,建立绑定关系
public class Constants {
public static final String ACK_QUEUE = "ack.queue";
public static final String ACK_EXCHANGE = "ack.exchange";
}
import com.amadeus.rabbitextensiondemo.constant.Constants;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitMQConfig {
//消息确认
@Bean("ackQueue")
public Queue RabbitMQConfig(){
return QueueBuilder.durable(Constants.ACK_QUEUE).build();
}
@Bean("directExchange")
public DirectExchange directExchange(){
return ExchangeBuilder.directExchange(Constants.ACK_EXCHANGE).build();
}
@Bean("ackBinding")
public Binding ackBinding(@Qualifier("directExchange") DirectExchange directExchange, @Qualifier("ackQueue") Queue queue){
return BindingBuilder.bind(queue).to(directExchange).with("ack");
}
}
设置生产者和消费者
producer
import com.amadeus.rabbitextensiondemo.constant.Constants;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/producer")
public class ProducerController {
@Autowired
private RabbitTemplate rabbitTemplate;
@RequestMapping("/ack")
public String ack(){
rabbitTemplate.convertAndSend(Constants.ACK_EXCHANGE,"ack","hello cheems");
System.out.println("发送成功");
return "success";
}
}
listener
import com.amadeus.rabbitextensiondemo.constant.Constants;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class AckListener {
//自动确认auto
@RabbitListener(queues = Constants.ACK_QUEUE)
public void handMessage(Message message) {
try {
System.out.printf("接收到消息: %s\n", new String(message.getBody(), "UTF-8"));
System.out.println("业务逻辑处理");
System.out.println("业务处理完成");
} catch (Exception e) {
System.err.println("消息处理失败: " + e.getMessage());
}
}
}
1.2 测试

消息一经发出就立刻ack了,
如果消费者处理消息异常时会如何呢? 我们不妨在自动确认的机制下尝试~

未成功被消费的消息就好似直接被丢弃了, (被遗弃消息: 我还没上车呐~ )
总结: 自动确认不能保证消息的可靠性
2. 手动确认:消费端可靠性的核心
手动确认模式下,消息状态会拆成两类:
| 控制台状态 | 含义 |
|---|---|
Ready | 还在队列中,等待投递给消费者 |
Unacked | 已经投递给消费者,但 RabbitMQ 还没收到确认信号 |
手动确认流程:
[Ready]
|
| RabbitMQ 投递给 Consumer
v
[Unacked]
|
| Consumer 处理成功 -> basicAck
v
[Deleted]
[Unacked]
|
| Consumer 断开连接 / Channel 关闭
v
[Requeue -> Ready]
Unacked 不是消息丢了,而是消息正在消费者手里“处理中”。
如果消费者挂掉,RabbitMQ 会把未确认消息重新入队,等待下一次投递。
2.1 手动确认代码实现
生产者代码基本无需处理,只需要调整两个地方 ,分别是确认策略和消费代码
yml文件配置信息
spring:
application:
name: rabbit-extensions-demo
rabbitmq:
addresses: amqp://"账号名":"密码"@"RabbitMQ服务器ip地址端口号"/"虚拟机名"
listener:
simple:
# acknowledge-mode: none #消息接收确认
# acknowledge-mode: auto #消息接收确认
acknowledge-mode: manual #消息接收确认: 手动确认
prefetch: 1 # 每次从队列中获取消息的条数
消费者处理
//手动确认
@RabbitListener(queues = Constants.ACK_QUEUE)
public void handMessage(Message message, Channel channel) throws Exception {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
//消费者逻辑
System.out.printf("接收到消息: %s, deliveryTag: %d \n", new String(message.getBody(),"UTF-8"), message.getMessageProperties().getDeliveryTag());
//进行业务逻辑处理
System.out.println("业务逻辑处理");
int num = 3/0;
System.out.println("业务处理完成");
//肯定确认
channel.basicAck(deliveryTag,false);
} catch (Exception e) {
//否定确认
channel.basicNack(deliveryTag, false, true);
}
}
按照这样的修改,发送了一场就会走否定确认,消息消费失败就不断尝试加入队列中
2.2 测试

如何限制消息消费的次数,防止日志刷屏, 避免消费者一直被占用呢? 这在后面会提及
二、Ready 与 Unacked:控制台状态深度剖析
RabbitMQ Management 控制台中,队列经常看到这几个数字:
| 指标 | 解释 | 常见原因 |
|---|---|---|
Ready 很高 | 消息堆在队列里,还没投递 | 消费者数量不足、消费太慢、没有消费者 |
Unacked 很高 | 消息已投递但未确认 | 业务处理慢、忘记 ack、线程阻塞、prefetch 过大 |
Ready=0, Unacked>0 | 消息都被消费者拿走了,但还没处理完 | 消费者可能卡住或确认逻辑异常 |
Ready 和 Unacked 都高 | 既有堆积,也有大量处理中消息 | 消费能力不足,需扩容或限流 |
一个非常典型的故障现场
Ready: 0
Unacked: 1000
Consumers: 1
这通常意味着:
- 消费者一次性拿走了大量消息;
- 没有及时
basicAck; - 或者业务逻辑卡住;
- RabbitMQ 不能继续安全删除这些消息。
此时不要第一反应重启 RabbitMQ。
更应该先检查消费者线程、日志异常、数据库慢查询、第三方接口超时,以及prefetch设置。
三、底层 API 详解
RabbitMQ Java Client 中,消费端确认主要有三个方法:
channel.basicAck(long deliveryTag, boolean multiple);
channel.basicReject(long deliveryTag, boolean requeue);
channel.basicNack(long deliveryTag, boolean multiple, boolean requeue);
1. deliveryTag:不是消息 ID,而是 Channel 内的投递编号
deliveryTag 是 RabbitMQ 给每次投递生成的编号。
| 特性 | 说明 |
|---|---|
| 单调递增 | 同一个 Channel 内从小到大递增 |
| Channel 级别唯一 | 不同 Channel 可以出现相同 deliveryTag |
| 必须同 Channel 确认 | A Channel 收到的消息,不能用 B Channel ack |
| 用于确认投递 | basicAck / basicReject / basicNack 都依赖它 |
错误示例:
// message 是 channel-1 收到的
// 却用 channel-2 ack
channel2.basicAck(deliveryTag, false);
后果:
unknown delivery tag
channel closed
deliveryTag 不是业务消息 ID。
业务去重请使用业务唯一键,例如orderNo、eventId、messageId。
2. basicAck:肯定确认
channel.basicAck(deliveryTag, multiple);
含义:
告诉 RabbitMQ:这条消息已经被我成功处理,可以删除了。
| 参数 | 类型 | 含义 |
|---|---|---|
deliveryTag | long | 要确认的投递编号 |
multiple | boolean | 是否批量确认 |
multiple=false
只确认当前这一条。
channel.basicAck(deliveryTag, false);
适合:
- 单条业务处理;
- 每条消息独立事务;
- 失败隔离要求高。
multiple=true
确认当前 deliveryTag 以及之前所有未确认的消息。
channel.basicAck(deliveryTag, true);
批量确认示意:
Unacked tags: 5, 6, 7, 8
basicAck(8, true)
Result:
5, 6, 7, 8 全部被确认并删除
图表占位:
注意:
- 批量确认能减少网络开销;
- 但如果前面的消息实际还没处理完,会造成误删;
- 因此只适合严格顺序处理且确认边界明确的场景。
3. basicReject:拒绝单条消息
channel.basicReject(deliveryTag, requeue);
含义:
告诉 RabbitMQ:这条消息我不处理了,你决定重新入队还是丢弃 / 死信。
| 参数 | 类型 | 含义 |
|---|---|---|
deliveryTag | long | 要拒绝的投递编号 |
requeue | boolean | 是否重新入队 |
requeue=true
channel.basicReject(deliveryTag, true);
消息重新进入队列,等待再次投递。
Unacked -> Ready -> Consumer
适合:
- 临时性异常;
- 数据库短暂不可用;
- 下游服务短暂超时。
风险:
如果错误是永久性的,例如消息格式错误,
requeue=true会制造无限重试。
requeue=false
channel.basicReject(deliveryTag, false);
消息不会重新入队。
| 是否配置 DLX | 结果 |
|---|---|
| 配置了死信交换机 | 进入死信队列 |
| 没配置死信交换机 | 被 RabbitMQ 丢弃 |
4. basicNack:增强版拒绝,支持批量
channel.basicNack(deliveryTag, multiple, requeue);
basicNack 是 RabbitMQ 对 AMQP 0-9-1 的扩展,用来弥补 basicReject 不能批量拒绝的问题。
| 参数 | 类型 | 含义 |
|---|---|---|
deliveryTag | long | 拒绝的投递编号 |
multiple | boolean | 是否批量拒绝 |
requeue | boolean | 是否重新入队 |
示例:
channel.basicNack(deliveryTag, false, true);
表示:
- 拒绝当前消息;
- 重新放回队列;
- 后续可能再次投递。
批量拒绝:
channel.basicNack(8, true, false);
含义:
Unacked tags: 5, 6, 7, 8
basicNack(8, true, false)
Result:
5, 6, 7, 8 全部拒绝
如果配置 DLX -> 进入死信队列
否则 -> 丢弃
图表占位:
四、basicAck、basicReject、basicNack 对比总表
| 方法 | 正 / 负确认 | 是否支持批量 | 是否支持重新入队 | 典型用途 |
|---|---|---|---|---|
basicAck | 正确认 | 是 | 否 | 业务处理成功,删除消息 |
basicReject | 负确认 | 否 | 是 | 拒绝单条消息 |
basicNack | 负确认 | 是 | 是 | 批量拒绝、批量重回队列、批量死信 |
参数组合速查:
| API | 参数组合 | 效果 |
|---|---|---|
basicAck(tag, false) | 单条 ack | 删除当前消息 |
basicAck(tag, true) | 批量 ack | 删除当前及之前未确认消息 |
basicReject(tag, true) | 单条拒绝并重回队列 | 可能再次消费 |
basicReject(tag, false) | 单条拒绝不重回 | 进入 DLX 或丢弃 |
basicNack(tag, false, true) | 单条 nack 并重回 | 适合临时失败 |
basicNack(tag, true, false) | 批量 nack 不重回 | 批量进入 DLX 或丢弃 |
五、Spring Boot 整合策略
Spring AMQP 提供了三种确认模式:
public enum AcknowledgeMode {
NONE,
MANUAL,
AUTO
}
配置示例:
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: manual
prefetch: 10
1. NONE
等价于 RabbitMQ 的自动确认。
| 行为 | 说明 |
|---|---|
| 是否发送 ack | 不发送 |
| Broker 视角 | 消息投递后立即成功 |
| 异常后是否重试 | 不会 |
| 风险 | 消费失败也可能丢消息 |
适合非核心链路。
2. AUTO
Spring AMQP 默认模式。
| 情况 | Spring 容器行为 |
|---|---|
| Listener 正常返回 | 自动 ack |
| Listener 抛异常 | 不正常 ack,交给容器异常策略处理 |
它比 NONE 安全,但要注意:
- 异常处理策略会影响是否 requeue;
- 可能出现失败消息反复投递;
- 必须结合重试、死信、异常分类一起设计。
3. MANUAL
生产环境最可控的方式。
@RabbitListener(queues = "order.queue")
public void onMessage(Message message, Channel channel) throws IOException {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
String body = new String(message.getBody(), StandardCharsets.UTF_8);
// 1. 参数校验
// 2. 幂等判断
// 3. 执行业务事务
// 4. 提交成功后 ack
channel.basicAck(deliveryTag, false);
} catch (BizNonRetryableException e) {
// 不可重试异常:进入死信队列或丢弃
channel.basicNack(deliveryTag, false, false);
} catch (Exception e) {
// 临时异常:可以重回队列,但要防止无限重试
channel.basicNack(deliveryTag, false, true);
}
}
推荐策略:
| 异常类型 | 示例 | 建议处理 |
|---|---|---|
| 参数错误 | JSON 格式不对、字段缺失 | basicNack(false, false) 进死信 |
| 幂等冲突 | 消息已处理 | basicAck |
| 临时异常 | DB 超时、RPC 超时 | 有限制地 requeue 或延迟重试 |
| 永久异常 | 业务状态非法 | 进入死信队列人工排查 |
图表占位:
六、常见坑点分析
坑 1:业务没处理完就 ack
错误写法:
channel.basicAck(deliveryTag, false);
doBusiness();
如果 doBusiness() 抛异常,消息已经被 RabbitMQ 删除。
正确顺序:
doBusiness();
channel.basicAck(deliveryTag, false);
坑 2:无限 requeue=true
channel.basicNack(deliveryTag, false, true);
如果消息本身就是坏数据,会出现:
消费 -> 异常 -> requeue -> 再消费 -> 再异常
结果:
- CPU 飙升;
- 日志刷屏;
- 队列吞吐被坏消息拖垮;
- 正常消息被阻塞。
好似
建议:
- 增加重试次数;
- 超过阈值后进入死信队列;
- 对不可恢复异常直接
requeue=false。
坑 3:multiple=true 用错导致误确认
channel.basicAck(deliveryTag, true);
如果前面的消息还没完成业务处理,就会被一起确认删除。
建议:
| 场景 | multiple 建议 |
|---|---|
| 单线程顺序消费 | 可谨慎使用 true |
| 并发处理 | 尽量使用 false |
| 每条消息独立事务 | 使用 false |
| 批处理且边界明确 | 可使用 true |
坑 4:忽略 prefetch 导致 Unacked 暴涨
prefetch 决定一个消费者最多可以同时持有多少未确认消息。
spring:
rabbitmq:
listener:
simple:
prefetch: 10
| prefetch | 效果 |
|---|---|
| 1 | 严格一条条处理,吞吐较低,顺序性较好 |
| 10 / 50 | 常见生产配置,吞吐与风险平衡 |
| 250 | Spring AMQP 较新的默认值,需结合业务处理能力评估 |
| 0 | 无限制,风险很高 |
Unacked可以理解为消费者手里的“在途消息窗口”。
窗口越大,吞吐可能越高,但故障恢复和内存压力也越大。
七、生产建议清单
| 建议 | 说明 |
|---|---|
核心业务使用 MANUAL | 确认边界由业务控制 |
| ack 放在事务成功之后 | 避免消息已删但业务失败 |
| 消费逻辑必须幂等 | RabbitMQ 保证至少一次投递,不保证只投一次 |
| 区分可重试和不可重试异常 | 不要所有异常都 requeue |
| 配置 DLX | 给失败消息留出口 |
| 设置合理 prefetch | 控制 Unacked 窗口 |
| 监控 Ready / Unacked | 这是排查消费问题的一线指标 |
| 避免跨 Channel ack | deliveryTag 只在当前 Channel 有效 |
总结
RabbitMQ 的消费端确认机制,本质上是在回答一个问题:
Broker 什么时候可以安全地删除一条消息?
答案取决于确认模式:
autoAck=true:投递出去就删除,快但不可靠;autoAck=false:消费者明确确认后删除,可靠但需要你设计好失败路径;basicAck:成功,删除;basicReject:单条拒绝,可选择重回队列;basicNack:增强拒绝,支持批量和重回队列;Ready:还没投递;Unacked:已投递但未确认。
真正成熟的 RabbitMQ 消费端,不是简单写一个 @RabbitListener,而是把 确认时机、异常分类、幂等控制、重试策略、死信队列、prefetch 窗口和监控指标放在一起设计。
这才是消息系统从「能跑」走向「可靠」的关键一步。

413

被折叠的 条评论
为什么被折叠?



