day02-RabbitMQ 2026-05-14

RabbitMQ

🏷生产者可靠性

📘生产者重连

有的时候由于网络波动,可能会出现客户端连接MQ失败的情况。通过配置我们可以开启连接失败后的重连机制:

xml 复制代码
spring:
  rabbitmq:
    connection-timeout: 1s # 设置MQ的连接超时时间
    template:
      retry:
        enabled: true # 开启超时重试机制
        initial-interval: 1000ms # 失败后的初始等待时间
        multiplier:1#失败后下次的等待时长倍数,下次等待时长=initial-interval *multiplier
        max-attempts: 3 # 最大重试次数

⚠️注意:当网络不稳定的时候,利用重试机制可以有效提高消息发送的成功率。不过SpringAMQP提供的重试机制是
阻塞式的重试,也就是说多次重试等待的过程中,当前线程是被阻塞的,会影响业务性能。
如果对于业务性能有要求,建议禁用重试机制。如果一定要使用,请合理配置等待时长和重试次数,当然也
可以考虑使用异步线程来执行发送消息的代码。


📘生产者确认

RabbitMQ了Publisher Confirm和Publisher Return两种确认机制。开启确机制认后,在MQ成功收到消息后会返回确

认消息给生产者。返回的结果有以下几种情况:

  • ✅消息投递到了MQ,但是路由失败。此时会通过PublisherReturn返回路由异常原因,然后返回ACK,告知投递成功

  • ✅临时消息投递到了MQ,并且入队成功,返回ACK,告知投递成功

  • ✅持久消息投递到了MQ,并且入队完成持久化,返回ACK,告知投递成功

  • ❌其它情况都会返回NACK,告知投递失败


📘SpringAMQP实现生产者确认

  1. 在publisher这个微服务的application.yml中添加配置:
xml 复制代码
spring:
  rabbitmq:
    publisher-confirm-type: correlated # 开启publisher confirm机制,并设置confirm类型
    publisher-returns: true # 开启publisher return机制

配置说明:

  • 这里publisher-confirm-type有三种模式可选:

    • none:关闭confirm机制

    • simple:同步阻塞等待MQ的回执消息

    • correlated: MQ异步回调方式返回回执消息

  1. 每个RabbitTemplate只能配置一个ReturnCallback,因此需要在项目启动过程中配置:
java 复制代码
@slf4j
@Configuration
public class CommonConfig implements ApplicationContextAware {

  @Override
  public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    // ERabbitTemplate
    RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
    // ReturnCallback
    rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
    log.info("消息发送失败,应答码{},原因{},交换机{},路由键{},消息{}",
    replyCode, replyText, exchange, routingKey, message.toString());
    });
  }
}  
  1. 发送消息,指定消息ID、消息ConfirmCallback,每一条消息都要配置ConfirmCallback
java 复制代码
@Test
void testPublisherConfirm() throws InterruptedException {
  // 1.创建CorrelationData
  CorrelationData cd = new CorrelationData();
  // 2.给Future添加ConfirmCallback
  cd.getFuture().addCallback(new ListenableFutureCallback<CorrelationData.Confirm>() {
    @Override
    public void onFailure(Throwable ex) {
    // 2.1.Future发生异常时的处理逻辑,基本不会触发
    log. error ("handle message ack fail", ex);
    }
    @Override
    public void onSuccess(CorrelationData.Confirm result) {
      // 2.2.Future接收到回执的处理逻辑,参数中的result就是回执内容
      if(result.isAck()){//result.isAck(),boolean类型,true代表ack回执,false 代表 nack回执
        log.debug("发送消息成功,收到ack!");
      }else{//result.getReason(),String类型,返回nack时的异常描述
        log.error("发送消息失败,收到 nack,reason:{}",result.getReason());
        }
     } 
  });

  //3.反送消息
  rabbitTemplate. convertAndSend("hmall.direct", "red1", "hello", cd);
}

SpringAMQP中生产者消息确认的几种返回值情况:

  • 消息投递到了MQ,但是路由失败。会return路由异常原因,返回ACK

  • 临时消息投递到了MQ,并且入队成功,返回ACK

  • 持久消息投递到了MQ,并且入队完成持久化,返回ACK

  • 其它情况都会返回NACK,告知投递失败

如何处理生产者的确认消息?

  • 生产者确认需要额外的网络和系统资源开销,尽量不要使用

  • 如果一定要使用,无需开启Publisher-Return机制,因为一般路由失败是

    自己业务问题

  • 对于nack消息可以有限次数重试,依然失败则记录异常消息


🏷MQ可靠性

在默认情况下,RabbitMQ会将接收到的信息保存在内存中以降低消息收发的延迟。这样会导致两个问题:

  • 一旦MQ宕机,内存中的消息会丢失
  • 内存空间有限,当消费者故障或处理过慢时,会导致消息积压,引发MQ阻塞

📘数据持久化

RabbitMQ实现数据持久化包括3个方面:

  • 交换机持久化
  • 队列持久化
  • 消息持久化
java 复制代码
@Test
void testPageOut() {
  Message message = MessageBuilder
      .withBody("hello".getBytes(StandardCharsets. UTF_8) )
      .setDeliveryMode(MessageDeliveryMode.PERSISTENT.build();
  for (int i= 0; i < 1000000; i++) {
    rabbitTemplate.convertAndSend( routingKey: "simple.queue", message);
  }
}

📘Lazy Queue

从RabbitMQ的3.6.0版本开始,就增加了Lazy Queue的概念,也就是惰性队列。

惰性队列的特征如下:

  • 接收到消息后直接存入磁盘而非内存(内存中只保留最近的消息,默认2048条)

  • 消费者要消费消息时才会从磁盘中读取并加载到内存

  • 支持数百万条的消息存储

在3.12版本后,所有队列都是Lazy Queue模式,无法更改。

要设置一个队列为惰性队列,只需要在声明队列时,指定x-queue-mode属性为lazy即可:

基于配置实现

java 复制代码
@Bean
public Queue lazyQueue() {
  return QueueBuilder
    .durable("lazy. queue")
    .lazy()//开启Lazy模式
    .build();
}

基于注解实现

Java 复制代码
@RabbitListener(queuesToDeclare = @Queue(
    name = "lazy.queue",
    durable = "true",
    arguments = @Argument(name = "x-queue-mode", value = "lazy")
))
public void listenLazyQueue(String msg) {
  log.info("接收到 lazy.queue的消息:{}",msg);
}

🏷消费者可靠性

📘消费者确认机制

为了确认消费者是否成功处理消息,RabbitMQ提供了消费者确认机制(Consumer Acknowledgement)。当消费者

处理消息结束后,应该向RabbitMQ发送一个回执,告知RabbitMQ自己消息处理状态。回执有三种可选值:

  • ack:成功处理消息,RabbitMQ从队列中删除该消息
  • nack:消息处理失败,RabbitMQ需要再次投递消息
  • reject:消息处理失败并拒绝该消息,RabbitMQ从队列中删除该消息

SpringAMQP已经实现了消息确认功能。并允许我们通过配置文件选择ACK处理方式,有三种方式:

  • none:不处理。即消息投递给消费者后立刻ack,消息会立刻从MQ删除。非常不安全,不建议使用
  • manual:手动模式。需要自己在业务代码中调用api,发送ack或reject,存在业务入侵,但更灵活
  • auto:自动模式。SpringAMQP利用AOP对我们的消息处理逻辑做了环绕增强,当业务正常执行时则自动返回ack.

当业务出现异常时,根据异常判断返回不同结果:

  • 如果是业务异常,会自动返回nack
  • 如果是消息处理或校验异常,自动返回reject
xml 复制代码
spring:
  rabbitmq:
    listener:
      simple:
        prefetch: 1
          acknowledge-mode: none # none,关闭ack;manual,手动ack;auto:自动ack

📘失败重试机制

当消费者出现异常后,消息会不断requeue(重新入队)到队列,再重新发送给消费者,然后再次异常,再次requeue

,无限循环,导致mq的消息处理飙升,带来不必要的压力。

我们可以利用Spring的retry机制,在消费者出现异常时利用本地重试,而不是无限制的requeue到mq队列:

xml 复制代码
spring:
  rabbitmq:
    listener:
      simple:
        prefetch: 1
        retry:
          enabled: true #开启消费者失败重试
          initial-interval:1000ms # 初始的失败等待时长为1秒
          multiplier: 1 # 下次失败的等待时长倍数,下次等待时长=multiplier * last-interval
          max-attempts: 3 # 最大重试次数
          stateless: true # true无状态;false有状态。如果业务中包含事务,这里改为false

在开启重试模式后,重试次数耗尽,如果消息依然失败,则需要有MessageRecoverer接口来处理,它包含三种不同

的实现:

  • RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认就是这种方式

  • ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队

  • RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机

将失败处理策略改为RepublishMessageRecoverer:

  1. 首先,定义接收失败消息的交换机、队列及其绑定关系,此处略:

  2. 然后,定义RepublishMessageRecoverer:

java 复制代码
@Bean
public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate) {
  return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");
}

消费者如何保证消息一定被消费?

  • 开启消费者确认机制为auto,由spring确认消息处理成功后返回ack,异常时返回nack

  • 开启消费者失败重试机制,并设置MessageRecoverer,多次重试失败后将消息投递到异常交换机,交由人工处理


📘业务幂等性

幂等是一个数学概念,用函数表达式来描述是这样的:f(x)=f(f(x))。在程序开发中,则是指同一个业务,执行一次或

多次对业务状态的影响是一致的。

方案一,是给每个消息都设置一个唯-id,利用id区分是否是重复消息:

  1. 每一条消息都生成一个唯一的id,与消息一起投递给消费者。

  2. 消费者接收到消息后处理自己的业务,业务处理成功后将消息ID保存到数据库

  3. 如果下次又收到相同消息,去数据库查询判断是否存在,存在则为重复消息放弃处理。

Java 复制代码
@Bean
public MessageConverter messageConverter(){
  // 1.定义消息转换器
  Jackson2JsonMessageConverter jjmc = new Jackson2JsonMessageGonverter();
  //2.配置自动创建消息id,用于识别不同消息,也可以在业务中基于ID判断是否是重复消息
  jjmc.setCreateMessageIds(true);
  return jjmc;
}

方案二,是结合业务逻辑,基于业务本身做判断。以我们的业务为例:我们要在支付后修改订单状态为已支付,应该在

修改订单状态前先查询订单状态,判断状态是否是未支付。只有未支付订单才需要修改,其它状态不做处理:


🏷延迟消息

延迟消息:

生产者发送消息时指定一个时间,消费者不会立刻收到消息,而是在指定时间之后才收到消息。

延迟任务:

设置在一定时间之后才执行的任务


📘死信交换机

当一个队列中的消息满足下列情况之一时,就会成为死信(dead letter):

  • 消费者使用basic.reject或basic.nack声明消费失败,并且消息的requeue参数设置为false

  • 消息是一个过期消息(达到了队列或消息本身设置的过期时间),超时无人消费

  • 要投递的队列消息堆积满了,最早的消息可能成为死信

如果队列通过dead-letter-exchange属性指定了一个交换机,那么该队列中的死信就会投递到这个交换机中。这个交

换机称为死信交换机(Dead Letter Exchange,简称DLX)。


📘延迟消息插件

RabbitMQ的官方也推出了一个插件,原生支持延迟消息功能。该插件的原理是设计了一种支持延迟消息功能的交换机

,当消息投递到交换机后可以暂存一定时间,到期后再投递到队列。

步骤:

  1. 声明交换机
java 复制代码
@RabbitListener(bindings = @QueueBinding(
          value = @Queue(name = "delay.queue", durable = "true"),
          exchange = @Exchange(name = "delay.direct", delayed = "true"),//添加delayed属性
          key = "delay"
public void listenDelayMessage(String msg) {
  log.info("接收到delay.queue的延迟消息:{}",msg);
}
Java 复制代码
@Bean
public DirectExchange delayExchange() {
  return ExchangeBuilder
        .directExchange ("delay.direct")
        .delayed()// 设置delay的属性为true
        .durable(true)// 持久化
        .build();
}
  1. 发送消息时需要通过消息头x-delay来设置过期时间:
Java 复制代码
@Test
void testPublisherDelayMessage() {
  // 1.创建消息
  String message = "hello, delayed message";
  // 2.发送消息,利用消息后置处理器添加消息头
  rabbitTemplate.convertAndSend("delay.direct", "delay", message, new MessagePostProcessor() {
    @Override
    public Message postProcessMessage (Message message) throws AmqpException {
      // 添加延迟消息属性
      message.getMessageProperties().setDelay(5000);
      return message;
    }
  });
}

🧾总结:今天学完了RabbitMQ的所有知识,对于消息队列感觉是回用就可以,不用太过于深究,真正要深入研究的是MySQL和redis这些,今后我也会继续学习,继续为大家分享学习感悟和有用的知识,加油!🔥🔥🔥

相关推荐
Nontee1 小时前
Java 后端面试题目全集
java·开发语言·面试
jran-1 小时前
Docker dockerfile镜像制作&compose服务编排&私有仓库
java·docker·容器
Chase_______1 小时前
【Java杂项】0.1 + 0.2 为什么不等于 0.3?IEEE 754 与 BigDecimal 精度避坑
java·开发语言·python
ch.ju1 小时前
Java Programming Chapter 4——Static part
java·开发语言
YDS8292 小时前
DeepSeek RAG&MCP + Agent智能体项目 —— 环境搭建和项目初始化
java·springboot·agent·rag·deepseek
ChoSeitaku2 小时前
04.数组
java·开发语言·数据结构
float_com2 小时前
【java进阶】------ 多线程【实际案例分析】
java
用户298698530142 小时前
Java 中的 Word 变量管理:添加、统计、获取与删除
java·后端
郭龙_Jack2 小时前
Java 17 到 Java 25:LTS 升级的全面收益与迁移指南
java·开发语言·python