[RabbitMQ高级特性] 消息确认机制:从 Ready / Unacked 到 basicAck、basicReject、basicNack 的底层拆解

前言

在 RabbitMQ 中,消息投递成功不等于业务处理成功

生产者把消息投递到 RabbitMQ,只解决了「消息进入 Broker」的问题;RabbitMQ 把消息推给消费者,也只解决了「消息到达消费者进程」的问题。真正困难的是:

消费者拿到消息后,业务逻辑执行到一半宕机怎么办?
消费者抛异常了,消息是丢弃、重试,还是进入死信队列?
控制台里的 ReadyUnacked 到底在表达什么?

这就是 Message Acknowledgement,消息确认机制要解决的问题。

RabbitMQ 官方文档也明确区分了两类确认机制:

方向机制作用
Producer -> RabbitMQPublisher Confirms确认消息是否成功到达 Broker
RabbitMQ -> ConsumerConsumer Acknowledgements确认消息是否被消费者成功处理

本文只聚焦后者:消费端确认机制


一、核心机制解析:自动确认与手动确认

RabbitMQ 在消费者订阅队列时,会通过 autoAck 决定确认模式。

channel.basicConsume(queueName, autoAck, consumer);
模式autoAckRabbitMQ 何时删除消息可靠性适用场景
自动确认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 测试

image.png

消息一经发出就立刻ack了,

如果消费者处理消息异常时会如何呢? 我们不妨在自动确认的机制下尝试~

image.png

未成功被消费的消息就好似直接被丢弃了, (被遗弃消息: 我还没上车呐~ )

总结: 自动确认不能保证消息的可靠性


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 测试

image.png

如何限制消息消费的次数,防止日志刷屏, 避免消费者一直被占用呢? 这在后面会提及


二、Ready 与 Unacked:控制台状态深度剖析

RabbitMQ Management 控制台中,队列经常看到这几个数字:

指标解释常见原因
Ready 很高消息堆在队列里,还没投递消费者数量不足、消费太慢、没有消费者
Unacked 很高消息已投递但未确认业务处理慢、忘记 ack、线程阻塞、prefetch 过大
Ready=0, Unacked>0消息都被消费者拿走了,但还没处理完消费者可能卡住或确认逻辑异常
ReadyUnacked 都高既有堆积,也有大量处理中消息消费能力不足,需扩容或限流

一个非常典型的故障现场

Ready: 0
Unacked: 1000
Consumers: 1

这通常意味着:

  • 消费者一次性拿走了大量消息;
  • 没有及时 basicAck
  • 或者业务逻辑卡住;
  • RabbitMQ 不能继续安全删除这些消息。

此时不要第一反应重启 RabbitMQ。
更应该先检查消费者线程、日志异常、数据库慢查询、第三方接口超时,以及 prefetch 设置。

basicAck

basicReject / basicNack, requeue=true

basicReject / basicNack, requeue=false 且配置 DLX

basicReject / basicNack, requeue=false 且未配置 DLX

Consumer 断开 / Channel 关闭

Producer 发送消息

Exchange 路由

Queue

Ready: 等待投递

RabbitMQ 投递给 Consumer

Unacked: 已投递, 未确认

消息删除

Dead Letter Exchange

Dead Letter Queue

消息丢弃


三、底层 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。
业务去重请使用业务唯一键,例如 orderNoeventIdmessageId


2. basicAck:肯定确认

channel.basicAck(deliveryTag, multiple);

含义:

告诉 RabbitMQ:这条消息已经被我成功处理,可以删除了。

参数类型含义
deliveryTaglong要确认的投递编号
multipleboolean是否批量确认
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 全部被确认并删除

图表占位:

Unacked 消息集合

deliveryTag=5

deliveryTag=6

deliveryTag=7

deliveryTag=8

basicAck(8, true)

确认 <= 8 的所有未确认消息

5, 6, 7, 8 全部删除

basicAck(8, false)

只确认 deliveryTag=8

5, 6, 7 仍然 Unacked

注意:

  • 批量确认能减少网络开销;
  • 但如果前面的消息实际还没处理完,会造成误删;
  • 因此只适合严格顺序处理且确认边界明确的场景。

3. basicReject:拒绝单条消息

channel.basicReject(deliveryTag, requeue);

含义:

告诉 RabbitMQ:这条消息我不处理了,你决定重新入队还是丢弃 / 死信。

参数类型含义
deliveryTaglong要拒绝的投递编号
requeueboolean是否重新入队
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 不能批量拒绝的问题。

参数类型含义
deliveryTaglong拒绝的投递编号
multipleboolean是否批量拒绝
requeueboolean是否重新入队

示例:

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 -> 进入死信队列
否则 -> 丢弃

图表占位:

true

false

false

true

true

false

basicReject(tag, requeue)

requeue?

重新入队 Ready

是否配置死信交换机 DLX?

进入死信队列

直接丢弃

basicNack(tag, multiple, requeue)

multiple?

只拒绝当前 tag

拒绝 <= tag 的所有未确认消息

requeue?


四、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 或延迟重试
永久异常业务状态非法进入死信队列人工排查

图表占位:

处理成功

可重试异常

不可重试异常

Spring Boot @RabbitListener 接收消息

获取 deliveryTag

执行业务逻辑

channel.basicAck(deliveryTag, false)

RabbitMQ 删除消息

channel.basicNack(deliveryTag, false, true)

消息重新入队 Ready

channel.basicNack(deliveryTag, false, false)

是否配置 DLX?

进入死信队列

消息丢弃


六、常见坑点分析

坑 1:业务没处理完就 ack

错误写法:

channel.basicAck(deliveryTag, false);
doBusiness();

如果 doBusiness() 抛异常,消息已经被 RabbitMQ 删除。

正确顺序:

doBusiness();
channel.basicAck(deliveryTag, false);

坑 2:无限 requeue=true

channel.basicNack(deliveryTag, false, true);

如果消息本身就是坏数据,会出现:

消费 -> 异常 -> requeue -> 再消费 -> 再异常

结果:

  • CPU 飙升;
  • 日志刷屏;
  • 队列吞吐被坏消息拖垮;
  • 正常消息被阻塞。

好似image.png

建议:

  • 增加重试次数;
  • 超过阈值后进入死信队列;
  • 对不可恢复异常直接 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常见生产配置,吞吐与风险平衡
250Spring AMQP 较新的默认值,需结合业务处理能力评估
0无限制,风险很高

Unacked 可以理解为消费者手里的“在途消息窗口”。
窗口越大,吞吐可能越高,但故障恢复和内存压力也越大。


七、生产建议清单

建议说明
核心业务使用 MANUAL确认边界由业务控制
ack 放在事务成功之后避免消息已删但业务失败
消费逻辑必须幂等RabbitMQ 保证至少一次投递,不保证只投一次
区分可重试和不可重试异常不要所有异常都 requeue
配置 DLX给失败消息留出口
设置合理 prefetch控制 Unacked 窗口
监控 Ready / Unacked这是排查消费问题的一线指标
避免跨 Channel ackdeliveryTag 只在当前 Channel 有效

总结

RabbitMQ 的消费端确认机制,本质上是在回答一个问题:

Broker 什么时候可以安全地删除一条消息?

答案取决于确认模式:

  • autoAck=true:投递出去就删除,快但不可靠;
  • autoAck=false:消费者明确确认后删除,可靠但需要你设计好失败路径;
  • basicAck:成功,删除;
  • basicReject:单条拒绝,可选择重回队列;
  • basicNack:增强拒绝,支持批量和重回队列;
  • Ready:还没投递;
  • Unacked:已投递但未确认。

真正成熟的 RabbitMQ 消费端,不是简单写一个 @RabbitListener,而是把 确认时机、异常分类、幂等控制、重试策略、死信队列、prefetch 窗口和监控指标放在一起设计。

这才是消息系统从「能跑」走向「可靠」的关键一步。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值