MQ高级

72 阅读5分钟

消息可靠性

1、消息丢失的可能性:

• 生产者发送消息时丢失: -- 生产者发送信息室连接 MQ失败 -- 生产者发送信息达到MQ后未找到交换机 -- 生产者发送消息到达MQ的交换机后,未找到合适的Queue -- 消息到达MQ后,处理消息的进程发生异常

• MQ导致消息丢失 -- 消息到达MQ,保存到队列后,尚未消费就突然宕机;

• 消费者处理消息时: -- 消息接受后尚未处理突然宕机 -- 消息接收后处理过程抛出异常

2、保证MQ的可靠性

  1. 保证生产消息的可靠性
  2. 确保MQ不会将消息弄丢
  3. 保证消费消息的可靠性

生产消息可靠性

生产者确认机制

1、publisher-confirm 消息成功投递到交换机,返回ack;消息未成功投递到交换机,返回nack(记录消息以及交换机等相关信息到数据库,后期可以编写任务去补偿发送,如定时任务) 2、publisher-return 未正确到达队列(消息成功投递单路由失败),返回ack及失败原因

3、开启生产者确认,在生产者模块中的配置文件中添加如下配置

spring:
  rabbitmq:
    publisher-confirm-type: correlated # 开启publisher confirm机制,并设置confirm类型
    publisher-returns: true # 开启publisher return机制

这里publisher-confirm-type中有三种模式可选,

  • none :关闭confirm机制

  • simple:同步阻塞等待MQ的回执(回调方法)

  • correlated: MQ异步回调返回回执;

实现方法:

  • 定义ConfirmCallback:ConfirmCallback可以在发送消息时指定,因为每个业务处理confirm成功与否逻辑不确定
package com.itheima.publisher.config;

import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ReturnedMessage;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;

@Slf4j
@AllArgsConstructor
@Configuration
public class MqConfig {
    private final RabbitTemplate rabbitTemplate;

    @PostConstruct
    public void init(){
        rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
            @Override
            public void returnedMessage(ReturnedMessage returned) {
                log.error("触发return callback,");
                log.debug("exchange: {}", returned.getExchange());
                log.debug("routingKey: {}", returned.getRoutingKey());
                log.debug("message: {}", returned.getMessage());
                log.debug("replyCode: {}", returned.getReplyCode());
                log.debug("replyText: {}", returned.getReplyText());
            }
        });
    }
}
  • 定义Return回调:每个RabbitTemplate只能配置一个ReturnCallback,因此需要在项目加载时配置

发送失败处理机制、

在ConfirmCallback中收到nack表示消息投递失败,ReturnCallback异常表示路由失败:

可以将消息记录到失败消息表,由定时任务进行发布,每隔10秒钟(可设置)执行获取失败消息重新发送,发送一次则在失败次数字段加一,达到3次停止自动发送由人工处理。

消息持久化

为了提升性能,默认情况下MQ的数据都是在内存存储的临时数据,重启后就会消失。为了保证数据的可靠性,必须配置持久化,包括:

  • 交换机持久化----默认就是持久化----durable默认就是true
  • 队列持久化----默认就是持久化-----durable默认就是true
  • 消息持久化-----默认就是持久化------在发送消息时,使用Message对象,并设置delivery-mode为持久化

消费者确认机制

  • 介绍

为了确认消费者是否成功处理消息,RabbitMQ提供了消费者确认机制(Consumer Acknowledgement)。即:当消费者处理消息结束后,应该向RabbitMQ发送一个回执,告知RabbitMQ消息处理状态。回执有三种可选值:

  • ack:成功处理消息,RabbitMQ从队列中删除该消息
  • nack:消息处理失败,RabbitMQ需要再次投递消息
  • reject:消息处理失败并拒绝该消息,RabbitMQ从队列中删除该消息
ack取值
  rabbitmq:
    listener:
      simple:
        acknowledge-mode: none # 关闭ack
  • none 只要消息到达消费者,消费者直接返回ack给MQ,MQ收到ack会把队列中消息删除
  • manual(手动ack);
  • auto:自动ack。自动ack。消费消息不出异常,返回ack给MQ。消费消息出异常了,返回nack,把消息重回队列

消息重复消费问题

何为幂等性?多次执行同一个操作,最终结果是一样的

在程序开发中,是指同一个业务,执行一次或多次对业务状态的影响是一致的。例如:

  • 根据id删除数据
  • 查询数据

但数据的更新往往不是幂等的,如果重复执行可能造成不一样的后果。比如:

  • 取消订单,恢复库存的业务。如果多次恢复就会出现库存重复增加的情况
  • 退款业务。重复退款对商家而言会有经济损失。

所以,我们要尽可能避免业务被重复执行,然而在实际业务场景中,由于意外经常会出现业务被重复执行的情况。例如:

  • 页面卡顿时频繁刷新导致表单重复提交
  • 服务间调用的重试
  • MQ消息的重复投递
解决方案

实例代码

pubic void handleMessage(Message message,Channel channel) throws Exception{
      String msg = new String(message.getBody());
      log.warn("接受到一个延迟消息{}"+msg);
      Long count = redisTemplate.opsForValue().increment(msg);
      if(count==1){
      //手动ack
      channel.basicAck(message.getMesagePropertied.getDeliveryTag(),false);
   }
}

死信交换机

概念:如果这个包含死信的队列配置了 dead-letter-exchange 属性,指定了一个交换机,那么队列中的死信就会投递到这个交换机中,而这个交换机称为 死信交换机 (Dead Letter Exchange,检查DLX)。

成为死信的几种情况:

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

  • 1.2、消息设置了过期时间,或者消息存放的队列设置了过期时间,超时无人消费

  • 1.3、要投递的队列消息满了,无法投递

  • 这些就是死信,然后会通过路由规则经过交换机路由到一个队列,这个交换机就叫死信交换机,这个队列就是死信队列

2.2、死信的路由过程

  • 消费者拒绝消费消息

  • 队列绑定了死信交换机

  • 死信队列有绑定的死信交换机

TTL

超时未消费,消息变成死信的两种情况:1、消息所在的队列设置了超时时间

2、消息本身设置了超时时间

延迟队列

概念:一种实现消费者延迟收到消息的模式

在RabbitMQ中,没有延迟队列的功能。可以使用 TTL + 死信队列 的方式实现延迟队列

装一个Delay插件,没有队列,不会出现无法消费的问题