rabbitmq unacked消息如何处理_RabbitMQ如何保证消息的可靠投递?

本文详细介绍SpringBoot与RabbitMQ的整合配置方法,包括依赖引入、基于注解的配置方式、消息确认机制及消息可靠投递策略。通过示例代码讲解如何实现消息的生产和消费。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

0f5c707b15d0e83a33ca92b384bb974a.png
在这里插入图片描述

Spring Boot整合RabbitMQ

github地址:
https://github.com/erlieStar/rabbitmq-examples

Spring有三种配置方式

  1. 基于XML

  2. 基于JavaConfig

  3. 基于注解

当然现在已经很少使用XML来做配置了,只介绍一下用JavaConfig和注解的配置方式

RabbitMQ整合Spring Boot,我们只需要增加对应的starter即可

 <dependency>
   <groupId>org.springframework.bootgroupId>
   <artifactId>spring-boot-starter-amqpartifactId>
 dependency>

基于注解

在application.yaml的配置如下

spring:
  rabbitmq:
    host: myhost
    port: 5672
    username: guest
    password: guest
    virtual-host: /

log:
  exchange: log.exchange
  info:
    queue: info.log.queue
    binding-key: info.log.key
  error:
    queue: error.log.queue
    binding-key: error.log.key
  all:
    queue: all.log.queue
    binding-key: '*.log.key'

消费者代码如下

@Slf4j
@Component
public class LogReceiverListener {

    /**
     * 接收info级别的日志
     */
    @RabbitListener(
            bindings = @QueueBinding(
                    value = @Queue(value = "${log.info.queue}", durable = "true"),
                    exchange = @Exchange(value = "${log.exchange}", type = ExchangeTypes.TOPIC),
                    key = "${log.info.binding-key}"
            )
    )
    public void infoLog(Message message) {
        String msg = new String(message.getBody());
        log.info("infoLogQueue 收到的消息为: {}", msg);
    }

    /**
     * 接收所有的日志
     */
    @RabbitListener(
            bindings = @QueueBinding(
                    value = @Queue(value = "${log.all.queue}", durable = "true"),
                    exchange = @Exchange(value = "${log.exchange}", type = ExchangeTypes.TOPIC),
                    key = "${log.all.binding-key}"
            )
    )
    public void allLog(Message message) {
        String msg = new String(message.getBody());
        log.info("allLogQueue 收到的消息为: {}", msg);
    }
}

生产者如下

@RunWith(SpringRunner.class)
@SpringBootTest
public class MsgProducerTest {

    @Autowired
    private AmqpTemplate amqpTemplate;
    @Value("${log.exchange}")
    private String exchange;
    @Value("${log.info.binding-key}")
    private String routingKey;

    @SneakyThrows
    @Test
    public void sendMsg() {
        for (int i = 0; i 5; i++) {
            String message = "this is info message " + i;
            amqpTemplate.convertAndSend(exchange, routingKey, message);
        }

        System.in.read();
    }
}

Spring Boot针对消息ack的方式和原生api针对消息ack的方式有点不同

原生api消息ack的方式

消息的确认方式有2种

自动确认(autoAck=true)
手动确认(autoAck=false)

消费者在消费消息的时候,可以指定autoAck参数

String basicConsume(String queue, boolean autoAck, Consumer callback)

autoAck=false: RabbitMQ会等待消费者显示回复确认消息后才从内存(或者磁盘)中移出消息

autoAck=true: RabbitMQ会自动把发送出去的消息置为确认,然后从内存(或者磁盘)中删除,而不管消费者是否真正的消费了这些消息

手动确认的方法如下,有2个参数

basicAck(long deliveryTag, boolean multiple)

deliveryTag: 用来标识信道中投递的消息。RabbitMQ 推送消息给Consumer时,会附带一个deliveryTag,以便Consumer可以在消息确认时告诉RabbitMQ到底是哪条消息被确认了。
RabbitMQ保证在每个信道中,每条消息的deliveryTag从1开始递增

multiple=true: 消息id<=deliveryTag的消息,都会被确认

myltiple=false: 消息id=deliveryTag的消息,都会被确认

消息一直不确认会发生啥?

如果队列中的消息发送到消费者后,消费者不对消息进行确认,那么消息会一直留在队列中,直到确认才会删除。
如果发送到A消费者的消息一直不确认,只有等到A消费者与rabbitmq的连接中断,rabbitmq才会考虑将A消费者未确认的消息重新投递给另一个消费者

Spring Boot中针对消息ack的方式

有三种方式,定义在AcknowledgeMode枚举类中

方式解释
NONE没有ack,等价于原生api中的autoAck=true
MANUAL用户需要手动发送ack或者nack
AUTO方法正常结束,spring boot 框架返回ack,发生异常spring boot框架返回nack

spring boot针对消息默认的ack的方式为AUTO。

在实际场景中,我们一般都是手动ack。

application.yaml的配置改为如下

spring:
  rabbitmq:
    host: myhost
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    listener:
      simple:
        acknowledge-mode: manual # 手动ack,默认为auto

相应的消费者代码改为

@Slf4j
@Component
public class LogListenerManual {

    /**
     * 接收info级别的日志
     */
    @RabbitListener(
            bindings = @QueueBinding(
                    value = @Queue(value = "${log.info.queue}", durable = "true"),
                    exchange = @Exchange(value = "${log.exchange}", type = ExchangeTypes.TOPIC),
                    key = "${log.info.binding-key}"
            )
    )
    public void infoLog(Message message, Channel channel) throws Exception {
        String msg = new String(message.getBody());
        log.info("infoLogQueue 收到的消息为: {}", msg);
        try {
            // 这里写各种业务逻辑
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
        }
    }
}

我们上面用到的注解,作用如下

注解作用
RabbitListener消费消息,可以定义在类上,方法上,当定义在类上时需要和RabbitHandler配合使用
QueueBinding定义绑定关系
Queue定义队列
Exchange定义交换机
RabbitHandlerRabbitListener定义在类上时,需要用RabbitHandler指定处理的方法

基于JavaConfig

既然用注解这么方便,为啥还需要JavaConfig的方式呢?
JavaConfig方便自定义各种属性,比如同时配置多个virtual host等

具体代码看GitHub把

RabbitMQ如何保证消息的可靠投递

一个消息往往会经历如下几个阶段

350bdd1bb7da861e48b88b49e8b9a8b9.png
在这里插入图片描述

所以要保证消息的可靠投递,只需要保证这3个阶段的可靠投递即可

生产阶段

这个阶段的可靠投递主要靠ConfirmListener(发布者确认)和ReturnListener(失败通知)
前面已经介绍过了,一条消息在RabbitMQ中的流转过程为
producer -> rabbitmq broker cluster -> exchange -> queue -> consumer

ConfirmListener可以获取消息是否从producer发送到brokerReturnListener可以获取从exchange路由不到queue的消息

我用Spring Boot Starter 的api来演示一下效果

application.yaml

spring:
  rabbitmq:
    host: myhost
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    listener:
      simple:
        acknowledge-mode: manual # 手动ack,默认为auto

log:
  exchange: log.exchange
  info:
    queue: info.log.queue
    binding-key: info.log.key

发布者确认回调

@Component
public class ConfirmCallback implements RabbitTemplate.ConfirmCallback {

    @Autowired
    private MessageSender messageSender;

    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        String msgId = correlationData.getId();
        String msg = messageSender.dequeueUnAckMsg(msgId);
        if (ack) {
            System.out.println(String.format("消息 {%s} 成功发送给mq", msg));
        } else {
            // 可以加一些重试的逻辑
            System.out.println(String.format("消息 {%s} 发送mq失败", msg));
        }
    }
}

失败通知回调

@Component
public class ReturnCallback implements RabbitTemplate.ReturnCallback {

    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        String msg = new String(message.getBody());
        System.out.println(String.format("消息 {%s} 不能被正确路由,routingKey为 {%s}", msg, routingKey));
    }
}

@Configuration
public class RabbitMqConfig {

    @Bean
    public ConnectionFactory connectionFactory(
            @Value("${spring.rabbitmq.host}") String host,
            @Value("${spring.rabbitmq.port}") int port,
            @Value("${spring.rabbitmq.username}") String username,
            @Value("${spring.rabbitmq.password}") String password,
            @Value("${spring.rabbitmq.virtual-host}") String vhost) {
        CachingConnectionFactory connectionFactory = new CachingConnectionFactory(host);
        connectionFactory.setPort(port);
        connectionFactory.setUsername(username);
        connectionFactory.setPassword(password);
        connectionFactory.setVirtualHost(vhost);
        connectionFactory.setPublisherConfirms(true);
        connectionFactory.setPublisherReturns(true);
        return connectionFactory;
    }

    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory,
                                         ReturnCallback returnCallback, ConfirmCallback confirmCallback) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setReturnCallback(returnCallback);
        rabbitTemplate.setConfirmCallback(confirmCallback);
        // 要想使 returnCallback 生效,必须设置为true
        rabbitTemplate.setMandatory(true);
        return rabbitTemplate;
    }
}

这里我对RabbitTemplate做了一下包装,主要就是发送的时候增加消息id,并且保存消息id和消息的对应关系,因为RabbitTemplate.ConfirmCallback只能拿到消息id,并不能拿到消息内容,所以需要我们自己保存这种映射关系。在一些可靠性要求比较高的系统中,你可以将这种映射关系存到数据库中,成功发送删除映射关系,失败则一直发送

@Component
public class MessageSender {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public final Map unAckMsgQueue = new ConcurrentHashMap<>();public void convertAndSend(String exchange, String routingKey, String message) {
        String msgId = UUID.randomUUID().toString();
        CorrelationData correlationData = new CorrelationData();
        correlationData.setId(msgId);
        rabbitTemplate.convertAndSend(exchange, routingKey, message, correlationData);
        unAckMsgQueue.put(msgId, message);
    }public String dequeueUnAckMsg(String msgId) {return unAckMsgQueue.remove(msgId);
    }
}

测试代码为

@RunWith(SpringRunner.class)
@SpringBootTest
public class MsgProducerTest {

    @Autowired
    private MessageSender messageSender;
    @Value("${log.exchange}")
    private String exchange;
    @Value("${log.info.binding-key}")
    private String routingKey;

    /**
     * 测试失败通知
     */
    @SneakyThrows
    @Test
    public void sendErrorMsg() {
        for (int i = 0; i 3; i++) {
            String message = "this is error message " + i;
            messageSender.convertAndSend(exchange, "test", message);
        }
        System.in.read();
    }

    /**
     * 测试发布者确认
     */
    @SneakyThrows
    @Test
    public void sendInfoMsg() {
        for (int i = 0; i 3; i++) {
            String message = "this is info message " + i;
            messageSender.convertAndSend(exchange, routingKey, message);
        }
        System.in.read();
    }
}

先来测试失败者通知

输出为

消息 {this is error message 0} 不能被正确路由,routingKey为 {test}
消息 {this is error message 0} 成功发送给mq
消息 {this is error message 2} 不能被正确路由,routingKey为 {test}
消息 {this is error message 2} 成功发送给mq
消息 {this is error message 1} 不能被正确路由,routingKey为 {test}
消息 {this is error message 1} 成功发送给mq

消息都成功发送到broker,但是并没有被路由到queue中

再来测试发布者确认

输出为

消息 {this is info message 0} 成功发送给mq
infoLogQueue 收到的消息为: {this is info message 0}
infoLogQueue 收到的消息为: {this is info message 1}
消息 {this is info message 1} 成功发送给mq
infoLogQueue 收到的消息为: {this is info message 2}
消息 {this is info message 2} 成功发送给mq

消息都成功发送到broker,也成功被路由到queue中

存储阶段

这个阶段的高可用还真没研究过,毕竟集群都是运维搭建的,后续有时间的话会把这快的内容补充一下

消费阶段

消费阶段的可靠投递主要靠ack来保证。
前文已经介绍了原生api ack的方式和Spring Boot框架ack的方式

总而言之,在生产环境中,我们一般都是单条手动ack,消费失败后不会重新入队(因为很大概率还会再次失败),而是将消息重新投递到死信队列,方便以后排查问题

总结一下各种情况

  1. ack后消息从broker中删除

  2. nack或者reject后,分为如下2种情况
    (1) reque=true,则消息会被重新放入队列
    (2) reque=fasle,消息会被直接丢弃,如果指定了死信队列的话,会被投递到死信队列

相关阅读

RabbitMQ系列教程一:消息中间件的诸侯征战史

全网最全RabbitMQ总结,别再说你不会RabbitMQ

<think>好的,我需要帮助用户解决RabbitMQ队列中ready状态消息过多的问题。首先,我得回忆一下RabbitMQ的基本知识。Ready状态的消息是指那些已经准备好被消费者接收但尚未被处理消息。如果这个数量过多,可能意味着消费处理速度跟消息的生产速度,或者有其他配置问题。 根据用户提供的引用,特别是引用[4],消息unacked变为ready的情况包括消费者断开连接、消息被拒绝且requeue为true,或者消息超时。所以,用户的问题可能是消费处理及时,或者消息被频繁重新入队。 接下来,我需要考虑可能的解决方案。首先,可能需要增加消费者的处理能力,比如增加消费者实例或者优化消费者的代码效率。另外,检查是否消息被反复重新入队的情况,比如消费处理失败后requeue消息,导致消息断回到队列,形成堆积。 另外,引用[2]提到了死信队列,如果消息被多次重新投递,可以设置重试次数上限,超过后进入死信队列,避免无限循环。同时,引用[3]提到可以通过代码获取队列中的消息数量,这可能有助于监控和调整消费者的数量。 还需要考虑预取计数(prefetch count)的设置。如果prefetch设置过高,消费者可能一次获取太多消息,导致其他消费者无法处理,而设置太低可能影响吞吐量。调整这个参数可以在处理速度和负载均衡之间找到平衡。 另外,消息的TTL(存活时间)设置也可能有用,如果消息在一定时间内未被处理,可以自动过期,减少队列压力。过这可能适用于允许消息丢失的场景。 还有,可能需要检查生产者的发送速率,是否突然有大量消息涌入,导致消费者来处理。可能需要引入流量控制或者削峰填谷的策略,比如使用中间缓存或者调整生产频率。 总结下来,可能的解决步骤包括:增加消费者数量,优化消费者代码,调整预取设置,设置消息重试上限和死信队列,监控消息数量,调整消息TTL,以及控制生产者速率。需要根据具体情况逐一排查和调整,确保系统稳定。</think>要减少RabbitMQ队列中处于$ready$状态的消息数量,可通过以下步骤优化: ### 一、提升消费处理能力 1. **增加消费者实例** 通过水平扩展消费者节点实现并行处理。例如,在Spring Boot中可通过`@RabbitListener`注解动态设置并发数: ```java @RabbitListener(queues = "myQueue", concurrency = "5-10") ``` 表示初始5个消费者,最大扩展到10个[^4]。 2. **优化消费端代码效率** 检查消费是否存在阻塞操作(如同步I/O),可改用异步处理或批量操作。例如将单条消息处理耗时从$100ms$降低到$50ms$,吞吐量可提升一倍。 ### 二、调整消息预取机制 3. **降低prefetch计数** 设置`spring.rabbitmq.listener.simple.prefetch=20`(默认250),避免单个消费者占用过多未确认消息: ```yaml spring: rabbitmq: listener: simple: prefetch: 20 ``` 公式$处理能力 = 消费者数量 \times (prefetch / 平均处理时间)$,需根据实际吞吐量调整。 ### 三、控制消息重入队列 4. **设置重试次数上限** 在消费者代码中添加重试计数器,超过阈值后将消息投递到死信队列(DLX): ```java if(retryCount > 3){ channel.basicReject(deliveryTag, false); // 重新入队 channel.basicPublish(dlxExchange, dlxRoutingKey, message); } ``` 需预先声明死信交换机和队列[^2]。 5. **禁用自动重试机制** 关闭自动重新入队(auto-requeue)功能: ```java channel.basicNack(deliveryTag, multiple, false); // 第三个参数为requeue ``` ### 四、实施流控策略 6. **配置队列最大长度** 声明队列时设置`x-max-length`参数: ```java args.put("x-max-length", 1000); // 超过长度时丢弃最旧消息 ``` 7. **设置消息TTL** 对生产者发布的消息添加过期时间: ```java AMQP.BasicProperties props = new AMQP.BasicProperties.Builder() .expiration("60000") // 60秒未消费则自动删除 .build(); ``` ### 五、监控与动态调整 8. **实时监控队列状态** 使用RabbitMQ Management API获取队列指标: ```bash GET /api/queues/{vhost}/{name} ``` 重点关注`messages_ready`和`messages_unacknowledged`值[^3]。 9. **动态扩展消费者** 当检测到`messages_ready > 1000`时,通过Kubernetes自动扩容消费者Pod,实现弹性伸缩。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值