RabbitMQ消息可靠性

RabbitMQ消息可靠性

生产者可靠性

生产者重连

  • Spring Boot 的 RabbitMQ 客户端(基于 RabbitTemplate)默认已经实现了连接恢复和重连机制。
  • 自动连接恢复
    • Spring AMQP 使用了 CachingConnectionFactory 来管理 RabbitMQ 连接。CachingConnectionFactory 内置了连接恢复机制,当连接断开时,它会自动尝试重新连接。
      • 重连次数:默认情况下,CachingConnectionFactory 会无限次重试。
        • 如果断开RabbitMQ容器会无限同步报错
      • 重连间隔:默认重连间隔为 5 秒。
  • 配置连接重连机制
    • 如果希望限制连接 RabbitMQ 失败后的重连次数,需要通过编程方式配置 CachingConnectionFactory,因为 Spring Boot 没有直接提供相关的配置属性。

生产者确认

  • 在默认情况下,生产者发送消息到 RabbitMQ 后,无法确定消息是否成功到达 RabbitMQ 服务器。如果 RabbitMQ 服务器崩溃或网络故障,可能会导致消息丢失。生产者确认机制通过以下方式解决这个问题:
    • 确认消息到达:RabbitMQ 服务器在接收到消息后,会向生产者发送一个确认(ack)。
    • 处理消息丢失:如果消息无法路由到队列(如交换机不存在或队列未绑定),RabbitMQ 会向生产者发送一个否定确认(nack)。
  • 确认消息到达的流程
    • 生产者发送消息
      • 生产者将消息发送到 RabbitMQ 服务器。
      • 消息会被发送到指定的交换机和路由键。
    • RabbitMQ 处理消息
      • RabbitMQ 服务器接收到消息后,会尝试将消息路由到绑定的队列。
      • 如果消息成功路由到队列,RabbitMQ 会将该消息持久化(如果队列和消息都配置了持久化)。
    • 发送确认(ack)
      • 如果消息成功到达 RabbitMQ 并被处理,RabbitMQ 会向生产者发送一个确认(ack)。
      • 确认中通常包含一个唯一的标识符(如 correlationId),用于标识是哪条消息被确认。
    • 生产者处理确认
      • 生产者接收到确认后,可以确定消息已经成功到达 RabbitMQ。
      • 生产者可以根据确认结果执行后续逻辑(如记录日志、更新状态等)。

MQ可靠性

  • 通过配置保证交换机、队列以及发送的所有消息进行持久化到磁盘,保证消息重启后不会丢失

    • @Bean
      public Queue directQueue01() {
          return new Queue("directQueue01", true);
      }
      
    • 通过new Queue("directQueue01", true)创建的队列是持久化的(true表示持久化)。这意味着即使RabbitMQ服务器重启,队列本身仍然存在。

    • 然而,队列的持久化并不保证队列中的消息也是持久化的。消息的持久化取决于消息发布时的设置。要确保消息持久化,需要在发布消息时设置消息的deliveryMode2(持久化模式)。

  • 使用惰性队列

    • 消息存储在磁盘
      • 惰性队列会将消息直接写入磁盘,而不是优先存储在内存中。
      • 普通队列会将消息存储在内存中,只有在内存不足时才会将消息写入磁盘。
      • 消息积压严重:当生产者发送消息的速度远大于消费者处理消息的速度时,惰性队列可以避免内存被大量占用。
  • 创建惰性队列

    • @Configuration
      public class RabbitMQConfig {
      
          @Bean
          public Queue lazyQueue() {
              return QueueBuilder.durable("lazyQueue")
                      .lazy() // 设置为惰性队列
                      .build();
          }
      }
      
    • 或者设置队列参数:x-queue-mode,值为 lazy

    • @Service
      public class LazyQueueListener {
      
          @RabbitListener(
                  bindings = @QueueBinding(
                          value = @Queue(
                                  name = "lazyQueue", // 队列名称
                                  durable = "true",  // 队列持久化
                                  arguments = @Argument(name = "x-queue-mode", value = "lazy") // 设置为惰性队列
                          ),
                          exchange = @Exchange(name = "directExchange", type = "direct"), // 绑定到交换机
                          key = "lazy.routing.key" // 路由键
                  )
          )
          public void listenLazyQueue(String message) {
              System.out.println("Received message from lazyQueue: " + message);
          }
      }
      
  • 注意事项

    • 只有队列和消息都设置为持久化,才能确保消息不会丢失。惰性队列不会改变消息的持久化属性,对于非持久化消息,即使它们被写入磁盘,RabbitMQ 也不会尝试恢复它们

      •     public void sendNonPersistentMessage(String message) {
                MessageProperties properties = new MessageProperties();
                properties.setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT); // 设置消息为非持久化
                Message msg = new Message(message.getBytes(), properties);
                rabbitTemplate.send("exchange", "routingKey", msg);
                System.out.println("Sent non-persistent message: " + message);
            }
        
    • 如果队列是非持久化的,即使消息是持久化的,队列在重启后也会丢失,消息自然也无法恢复。

    • 持久化只能确保在 RabbitMQ 正常关闭或重启时消息不丢失。

  • 如果队列是持久化的,那么发送到该队列中的消息默认就是持久化的

  • 下表为MQ路由到死信队列的机制

情况 结果
没有消费者消费消息 消息一直存储在队列中,直到被消费或过期。
消息过期(TTL) 消息被移除,如果配置了死信队列,会被路由到死信队列。
队列达到最大长度 最早的消息被移除,如果配置了死信队列,会被路由到死信队列。
队列被删除 队列中的所有消息被清除。
RabbitMQ 服务器重启 持久化的队列和消息会保留,非持久化的队列和消息会丢失。
配置了死信队列(DLX) 消息被拒绝、过期或达到队列最大长度时,会被路由到死信队列。

消费者可靠性

消息的回执

  • ack(Acknowledge)
    • 含义ack表示消息已被成功处理。
    • 行为
      • RabbitMQ会将该消息从队列中移除。
      • 消息不会被重新投递。
  • nack(Negative Acknowledge)
    • 含义nack表示消息处理失败,但可能需要重试。
    • 行为
      • RabbitMQ会根据配置决定是否重新将消息放回队列(重新投递)。
      • 可以一次性nack多条消息(批量处理时)。
  • reject
    • 含义reject表示消息处理失败,且无需重试。
    • 行为
      • RabbitMQ会直接丢弃消息或将其转移到死信队列(如果配置了死信队列)。
      • 只能拒绝单条消息(不能批量拒绝)。

消息确认模式

  • 消息确认模式决定了消费者如何处理消息的确认行为,分为以下三种:

  • 默认模式AUTO

    • none:不处理

      • 行为:消息一旦被消费者接收,Spring AMQP会自动发送ack给消息代理,消息代理会立即将消息标记为已处理并从队列中移除。

      • 在此模式下,消息发送后不会等待任何确认。无论消费者是否成功处理消息,RabbitMQ都会认为消息已被处理。

      • 特点

        • 实现简单,适合对消息丢失不敏感的场景。
        • 如果消费者在处理消息时发生异常,消息可能会丢失,因为消息已经被确认。
    • Manual模式

      • 描述:在此模式下,开发者需手动确认消息处理结果。需要显式发送acknackreject
    • Auto模式

      • 描述:在此模式下,Spring AMQP会自动确认消息。如果消息处理过程中未抛出异常,则自动发送ack;若抛出异常,则根据配置发送nackreject
      • 如果是业务异常,则返回nack
      • 如果是消息处理或者校验异常,则返回reject
  • 配置消息确认模式

    • 消息确认模式通过spring.rabbitmq.listener.simple.acknowledge-mode属性来设置。

    • spring:
        rabbitmq:
          listener:
            simple:
              acknowledge-mode: auto # 可选值:none、manual、auto
      
  • 失败重试机制

    • AUTO模式下,如果消费者返回nack或抛出异常,默认情况下消息会无限重发(即RabbitMQ会不断将消息重新放回队列并投递给消费者)。为了避免消息无限重发,可以通过配置重试机制来限制消息的重发次数。

    • spring:
        rabbitmq:
          listener:
            simple:
              retry:
                enabled: true # 开启重试机制
                max-attempts: 3 # 最大重试次数(包括第一次)
                initial-interval: 1000 # 初始重试间隔(毫秒)
                multiplier: 2.0 # 重试间隔倍数(每次重试间隔 = 上次间隔 * multiplier)
                max-interval: 10000 # 最大重试间隔(毫秒)
      
    • 默认行为

      • 如果重试次数用尽,Spring AMQP会记录一条错误日志,并丢弃(reject)消息
      • 如果队列配置了 死信交换器(Dead-Letter Exchange, DLX),消息会被转发到死信队列(Dead-Letter Queue, DLQ)。
      • 如果未配置 DLX,消息会被 直接丢弃(可能造成数据丢失,需谨慎)。
  • 自定义错误处理策略

    • 可以通过实现 MessageRecoverer 接口定义重试失败后的行为。

    • 首先配置死信交换机和死信队列并进行绑定,然后注入MessageRecoverer

    • @Bean
      public MessageRecoverer messageRecoverer(RabbitTemplate rabbitTemplate) {
          return new RepublishMessageRecoverer(rabbitTemplate, "error-exchange", "error-routing-key");
      }
      
    • 通过实现 MessageRecoverer 接口定义的重试失败处理逻辑,默认情况下会全局生效(即所有队列的消费者失败后都会走该逻辑)。

    • 与死信队列(DLQ)的配合

      如果队列已配置 DLQ,重试失败的消息会优先进入 DLQ,而不会触发 MessageRecoverer。需根据业务需求选择方案。

消费者网络中断的影响

  • 消费者连接断开:如果消费者与RabbitMQ之间的网络中断(如断网、宕机),RabbitMQ会检测到连接丢失。

  • 自动重新入队:连接断开后,所有未确认的消息(包括未收到ACK/NACK的消息)会被自动重新放回队列,等待其他消费者处理。这是RabbitMQ的默认行为,确保消息不丢失。

  • 消息是否会一直等待?

    • 短期等待:在网络中断期间,消息会短暂处于“未确认”状态,但一旦RabbitMQ检测到消费者断开连接,就会立即将消息重新入队。
    • 不会无限阻塞:RabbitMQ不会永久等待消费者响应。通过心跳检测(Heartbeat)或TCP超时机制,MQ能快速发现断连,触发消息重新入队。
  • 心跳机制(Heartbeat)

    • 启用心跳(默认开启)可更快检测断连。例如,设置心跳间隔为60秒:

    • spring:
        rabbitmq:
          connection-timeout: 1000
          requested-heartbeat: 60
      

消费者处理超时的场景

  • 如果消费者获取了消息但未在 TTL 时间内处理完毕:
  • 消息状态:消息处于“未确认”状态(Unacknowledged)。
  • TTL 触发:即使消息正在被处理,只要存活时间超过 TTL,RabbitMQ 会强制将其标记为过期。
  • 结果
    • 消息会被删除或转发到死信队列(如果配置了 DLX)。
    • 消费者后续的确认操作(ACK/NACK)将失效,因为消息已不存在。

业务幂等性

  • 幂等性(Idempotence) 是指同一操作多次执行所产生的影响与一次执行的影响相同。无论操作执行一次还是多次,系统的状态都保持一致。简单来说,就是“重复调用不改变结果”。

  • 为什么需要幂等性?

    • 在分布式系统、网络通信或高并发场景中,以下情况可能导致重复操作:
      • 网络重传:客户端未收到响应,自动重试请求。
      • 消息队列重复投递:消息中间件可能因ACK超时重新发送消息。
      • 用户重复提交:用户短时间内多次点击提交按钮。
    • 若不保证幂等性,可能导致:
      • 重复扣款(支付系统)
      • 重复下单(电商系统)
      • 数据不一致(数据库操作)
  • 幂等性的实现方案

    • 唯一标识符(ID)
      • 核心思想:为每个操作分配唯一标识(如订单号、请求ID),通过标识符去重。
        • 数据库唯一索引:利用数据库的唯一约束拦截重复数据插入。
      • 使用订单号作为唯一标识,支付前检查订单状态。
      • 调用第三方支付接口时,传递唯一out_trade_no,确保同一订单只扣款一次。
    • 通过业务逻辑实现幂等性

      • 数据库状态字段:在更新时校验当前状态。
    • Token 机制(一次性令牌)

      • 核心思想:服务端生成一次性 Token,客户端提交时携带,校验后失效。
  • 全局唯一ID的生成

    • 方案
      • UUID:简单但无序,可能影响数据库性能。
      • 雪花算法(Snowflake):生成有序递增的ID(如Twitter的分布式ID生成方案)。
      • 数据库自增序列:需分库分表时使用步长隔离。
posted @ 2025-03-26 18:31  QAQ001  阅读(112)  评论(0)    收藏  举报