05-RabbitMQ消息可靠性问题解决

1.消息丢失

消息从生产者到exchange再到queue,再到消费者,有哪些丢失消息的可能性?

(1)发送时丢失:

  生产者未发送到exchange丢失;

  生产者发送到exchange未发送到queue丢失;

(2)MQ宕机,queue消息丢失

(3)consumer接收到消息后未消费就宕机

2.消息丢失解决方案

  (1)生产者消息确认机制;

  (2)消息持久化

  (3)消费者确认机制

  (4)消费失败重试机制

2.1生产者消息确认机制

RabbitMQ提供了publisher confirm机制来避免消息发送到MQ过程中丢失。消息发送到MQ后,会返回一个结果给发送者,表示消息是否处理成功。结果有两种请求:

  &publisher-confirm,发送者确认:

    消息成功投递到交换机,返回ack。

    消息未投递到交换机,返回nack。

  &publisher-return,发送者回执:

    消息投递到交换机了,但是没有路由到队列,返回ack,及路由失败原因。

 

 注意:确认机制发送消息时,需要给消息设置一个全局的唯一id,区分不同的消息,避免ack冲突。

 2.2springAMQP实现生产者确认

(1)在生产者spplication.yml中添加配置:

spring:
  rabbitmq:
    publisher-confirm-type: correlated
    publisher-returns: true
    template:
      mandatory: true

配置说明:

  &publisher-confirm-type,开启publisher-confirm,这里支持两种类型:

    simple:同步等待confirm结果,直到超时

    correlated:异步回调,定义confirmCallback,MQ返回结果是会调用confirmCallback方法。

  &publisher-returns:开启publisher-return功能,同样是基于callback回调机制,只不过回调的是returnCallback。

  &template.mandatory:定义消息路由失败时的策略。false,直接丢弃消息,true,回调returnCallback。

(2)配置ReturnCallBack

  每个RabbitTemplate只能配置一个returnCallback,因此需要在项目启动时配置:

@Slf4j
@Configuration
public class CommonConfig implements ApplicationContextAware {

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        // 获取RabbitTemplate对象
        RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
        // 配置ReturnCallback
        rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
            // 记录日志
            log.error("消息发送到队列失败,响应码:{}, 失败原因:{}, 交换机: {}, 路由key:{}, 消息: {}",
                     replyCode, replyText, exchange, routingKey, message.toString());
            // 如果有需要的话,重发消息
        });
    }
}

(3)publish-confirm消息确认

    @Test
    public void testSendMessage2SimpleQueue() throws InterruptedException {
        // 1.准备消息
        String message = "hello, spring amqp!";
        // 2.准备CorrelationData
        // 2.1.消息ID
        CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
        // 2.2.准备ConfirmCallback
        correlationData.getFuture().addCallback(result -> {
            // 判断结果
            if (result.isAck()) {
                // ACK
                log.debug("消息成功投递到交换机!消息ID: {}", correlationData.getId());
            } else {
                // NACK
                log.error("消息投递到交换机失败!消息ID:{}", correlationData.getId());
                // 重发消息
            }
        }, ex -> {
            // 记录日志
            log.error("消息发送失败!", ex);
            // 重发消息
        });
        // 3.发送消息
        rabbitTemplate.convertAndSend("amq.topic", "a.simple.test", message, correlationData);
    }

总结:

springAMQP处理消息确认的几种情况:

  &publisher-confirm:

    消息成功发送到exchange,返回ack;

    消息发送失败没有发送到exchange,返回nack;

    消息发送过程中发送异常,没有回收回执。

  &publisher-returns:

    消息成功发送到exchange,但没有路由到queue,调用returnCallback;

2.3消息持久化

(1)交换机持久化

@Bean
public DirectExchange simpleExchange(){ //三个参数:交换机名称、是否持久化、当没有queue与其绑定时是否自动删除 return new DirectExchange("simple.direct", true, false);}

(2)队列持久化

@Bean
public Queue simpleQueue(){ // 使用QueueBuilder构建队列,durable就是持久化的
return QueueBuilder.durable("simple.queue").build();}

(3)消息持久化

  Message msg = MessageBuilder.withBody(message.getBytes(StandardCharsets.UTF_8))//消息体
            .setDeliveryMode(MessageDeliveryMode.PERSISTENT) // 持久化
            .build();

2.4消费者消息确认

 RabbitMQ支持消费者确认模式,即消费者处理消息后会向MQ发送ack回执,MQ收到ack回执才会删除消息。springAMQP支持三种消息确认模式:

  &manual:手动ack,需要在也去代码结束后,调用api发送ack;

  &auto:自动ack,由spring监测RabbitListener,没有异常返回ack,抛出异常返回nack;

  &none:关闭ack,MQ假定消费者获取消息后会处理成功,,因此消息投递成功后会立即删除;

spring:
  rabbitmq:
    listener:
      simple:
        prefetch: 1
        acknowledge-mode: auto

2.5失败重试机制

当消费者出现异常时,消息会不断requeue(重新入队)到队列,再重新发送给消费者,再次异常,再次requeue,导致无限循环,导致MQ消息处理飙升,带来不必要的压力。

可以使用spring的retry机制在消费者出现异常时利用本地重试,而不是无限制的重新requeue。

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

2.6消费者失败消息处理策略

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

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

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

 

 

测试下RepublishMessageRecoverer处理模式:

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

   @Bean
    public DirectExchange errorMessageExchange(){
        return new DirectExchange("error.direct");
    }

    @Bean
    public Queue errorQueue(){
        return new Queue("error.queue");
    }

    @Bean
    public Binding errorMessageBinding(){
        return BindingBuilder.bind(errorQueue()).to(errorMessageExchange()).with("error");
    }

  &然后,定义RepublishMessageRecoverer:

   

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

 

posted @ 2023-04-10 10:09  老王的日常  阅读(59)  评论(0)    收藏  举报