srpingboot+rabbitmq终极版

344 阅读11分钟
  • rabbitmq流程图

    img

  • rabbitmq主要概念

    1. 虚拟主机:一个虚拟主机持有一组交换机、队列和绑定。为什么需要多个虚拟主机呢?很简单,RabbitMQ当中,用户只能在虚拟主机的粒度进行权限控制。 因此,如果需要禁止A组访问B组的交换机/队列/绑定,必须为A和B分别创建一个虚拟主机。每一个RabbitMQ服务器都有一个默认的虚拟主机“/”。
    2. 交换机:Exchange 用于转发消息,但是它不会做存储 ,如果没有 Queue bind 到 Exchange 的话,它会直接丢弃掉 Producer 发送过来的消息。 这里有一个比较重要的概念:路由键 。消息到交换机的时候,交互机会转发到对应的队列中,那么究竟转发到哪个队列,就要根据该路由键。
    3. 绑定:也就是交换机需要和队列相绑定,这其中如上图所示,是多对多的关系。
  • exchange常用类型:

    1. direct:direct 类型的行为是"先匹配, 再投送". 即在绑定时设定一个 routing_key, 消息的routing_key 匹配时, 才会被交换器投送到绑定的队列中去.

    2. topic:按规则转发消息(最灵活)

      topic交换机规则简介:

      *表示一个单词(必须出现,单词间用 句号 . 分割)

      #表示零个或多个单词

      通配的绑定键是跟队列进行绑定的 若队列Q1 绑定键为 .TT. 队列Q2绑定键为 TT.# 如果一条消息携带的路由键为 A.TT.B,那么队列Q1将会收到; 如果一条消息携带的路由键为TT.AA.BB,那么队列Q2将会收到;

    3. fanout:消息广播的模式,不管路由键或者是路由模式,会把消息发给绑定给它的全部队列,如果配置了routing_key会被忽略。

    4. headers:也是根据规则匹配, 相较于 direct 和 topic 固定地使用 routing_key , headers 则是一个自定义匹配规则的类型. 在队列与交换器绑定时, 会设定一组键值对规则, 消息中也包括一组键值对( headers 属性), 当这些键值对有一对, 或全部匹配时, 消息被投送到对应队列.

  • rabbitmq的安装

    1. 查看镜像是否存在,选择有管理界面的版本

      docker search rabbitmq:management
      
    2. 拉去镜像

       docker pull rabbitmq:management
      
    3. 发布镜像

      docker run -d --name rabbitmq -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin -p 15672:15672 -p 5672:5672 -p 25672:25672 -p 61613:61613 -p 1883:1883 rabbitmq:management
      

      参数说明

      -d 后台运行容器;

      --name 指定容器名;

      -p 指定服务运行的端口(5672:应用访问端口;15672:控制台Web端口号)

      -v 映射目录或文件;

      --hostname 主机名(RabbitMQ的一个重要注意事项是它根据所谓的 “节点名称” 存储数据,默认为主机名);

      -e 指定环境变量;(RABBITMQ_DEFAULT_VHOST:默认虚拟机名;RABBITMQ_DEFAULT_USER:默认的用户名;RABBITMQ_DEFAULT_PASS:默认用户名的密码)

    4. 查看容器是否启动

      docker ps
      
    5. 打开界面管理http://Server-IP:15672,使用默认用户名密码登陆即可

  • 项目主要文件介绍

    1. 配置文件

      spring:
        rabbitmq:
          host: localhost
          port: 5672
          username: username
          password: password
          #手动确认
          listener:
            simple:
              acknowledge-mode: manual
              #是否支持重试
              retry:
                enabled: true
          #springboot2.2.0以前配置
          #publisher-confirms=true
          #springboot2.2.0以后
          #阶段一Producer-->Broker/Exchange confirmCallBack确认阶段
          publisher-confirm-type: correlated
          #阶段二Exchange --> Queue returnCallback回调确认阶段
          publisher-returns: true
          # 官方文档说此时这一项必须设置为true
          # 实际上这一项的作用是:消息【未成功到达】队列时,能监听到到路由不可达的消息,以异步方式优先调用我们自己设置的returnCallback,默认情况下,这个消息会被直接丢弃,无法监听到
          template:
            mandatory: true
      

      type取值

      • none:默认值,不开启confirmcallback机制
      • correlated:开启confirmcallback,发布消息时,可以指定一个CorrelationData,会被保存到消息头中,消息投递到Broekr时触发生产者指定的ConfirmCallback,这个值也会被返回,以进行对照处理,CorrelationData可以包含比较丰富的元信息进行回调逻辑的处理。无特殊需求,就设定为这个值。
      • simple模式:其一效果和correlated值一样能触发回调方法,其二用于发布消息成功后使用rabbitTemplate调用waitForConfirms或waitForConfirmsOrDie方法等待broker节点返回发送结果,需求根据返回结果来判定下一步的逻辑,执行更复杂的业务。要注意的点是waitForConfirmsOrDie方法如果返回false则会关闭channel,则接下来无法发送消息。

      acknowledge-mode取值

    2. 配置

      package com.bike.rabbitmq.config;
      
      import org.springframework.amqp.core.*;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      
      @Configuration
      public class RabbitmqConfig {
      
          @Bean
          DirectExchange consumerExchange(){
              // durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
              // exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
              // autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。
              //一般设置一下队列的持久化就好,其余两个就是默认false
              return ExchangeBuilder.directExchange("consumerExchange").durable(true).build();
          }
      
          @Bean
          Queue queue(){
              //durable 是否持久化
              return QueueBuilder.durable("consumer_route").build();
          }
      
          @Bean
          Binding bindConsumerExchange(){
              //队列绑定交换机,
              return BindingBuilder.bind(queue()).to(consumerExchange()).with("consumer_route_key");
          }
      
      }
      
    3. 生产者

      package com.bike.rabbitmq.controller;
      
      import com.bike.rabbitmq.service.ConfirmCallbackService;
      import com.bike.rabbitmq.service.ReturnsCallBackService;
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.amqp.AmqpException;
      import org.springframework.amqp.rabbit.core.RabbitTemplate;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.web.bind.annotation.GetMapping;
      import org.springframework.web.bind.annotation.RequestMapping;
      import org.springframework.web.bind.annotation.RestController;
      
      import java.util.UUID;
      
      @RestController
      @RequestMapping(value ="/provider")
      @Slf4j
      public class ProviderController{
      
          @Autowired
          private RabbitTemplate rabbitTemplate;
      
          @Autowired
          private ConfirmCallbackService confirmCallbackService;
      
          @Autowired
          private ReturnsCallBackService returnsCallBackService;
      
          @GetMapping
          public void provider(){
      
              try {
                  String uuid = UUID.randomUUID().toString();
                  String message = "hello mq " + uuid;
                  log.info("send message : {}",message);
                  //设消费者确认收到消息后,手动ack回执回调处理
                  rabbitTemplate.setConfirmCallback(confirmCallbackService);
                  //消息投递到队列失败回调处理
                  rabbitTemplate.setReturnsCallback(returnsCallBackService);
                  //发送交换机名称,路由key,消息内容
                  rabbitTemplate.convertAndSend("consumerExchange","consumer_route_key",message);
              } catch (AmqpException e) {
                  e.printStackTrace();
              }
      
          }
      
      }
      
      
    4. 消息的确认机制

      • 机制

        消息从发送到签收的整个过程是Producer-->Broker/Exchange-->Broker/Queue-->Consumer因此如果只是要保证消息的可靠投递,我们需要考虑的仅是前两个阶段,因为消息只要成功到达队列,就算投递成功。

        • 比如投递消息时指定的Exchange不存在,那么阶段一就会失败(confirmCallback确认模式)

        • 如果投递到Exchange成功,但是指定的路由件错误或者别的原因,消息没有从Exchange到达Queue,那就是第二阶段出错。(returnCallback退回模式)

          在这里插入图片描述

        而从生产者和消费者角度来看,消息成功投递到队列才算成功投递,因此阶段一和阶段二都属于生产者一方需要关注,阶段三属于消费者一方,这里只考虑消息的成功投递,因此不考虑消费者的签收部分。而Rabbitmq和springboot整合时,默认是没有开启消息确认的。

      • 使用ConfirmCallback Producer-->Broker/Exchange确认

        springboot自动装配了RabbitTemplate,但是因为默认没有开启消息确认机制,因此注入时并未设置confirmCallback属性,如果要使用确认机制,需要手动配置RabbitTemplate.ConfirmCallback,通过源码可以看出是一个被@FunctionalInterface注解修饰的接口,那么我们只需要实现其中的confirm方法实现自己的业务逻辑即可

        @FunctionalInterface
            public interface ConfirmCallback {
                void confirm(@Nullable CorrelationData var1, boolean var2, @Nullable String var3);
            }
        
        package com.bike.rabbitmq.controller;
        
        import lombok.extern.slf4j.Slf4j;
        import org.springframework.amqp.rabbit.connection.CorrelationData;
        import org.springframework.amqp.rabbit.core.RabbitTemplate;
        import org.springframework.stereotype.Component;
        
        @Component
        @Slf4j
        public class ConfirmCallbackService implements RabbitTemplate.ConfirmCallback{
        
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String s) {
                /**
                 * correlationData:对象内部只有一个 id 属性,用来表示当前消息的唯一性。
                 *
                 * ack:消息投递到broker 的状态,true表示成功。
                 *
                 * cause:表示投递失败的原因。
                 */
        //        log.info("return message : {}",correlationData.getReturned());
                log.info("boolean : {}",ack);
                log.info("string message : {}",s);
            }
        }
        
        
        

        如果消息确认统一处理,那么我们可以统一配置处理,不要在代码中单独的去处理

        @Configuration
        @Slf4j
        public class RabbitConfig {
        
            @Autowired
            RabbitTemplate rabbitTemplate;
        
         	// 方法名无所谓,主要是 @PostConstruct 指定它一定会被回调
            @PostConstruct
            public void setCallback() {
                /**
                 * 为容器创建好的rabbitTemplate注册confirmCallback
                 * 消息由生产者投递到Broker/Exchange回调
                 */
                rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
                	/**
                     * @param correlationData 发送消息时指定的唯一关联数据(消息id)
                     * @param ack 这个消息是否成功投递到Exchange
                     * @param cause 失败的原因
                     */
                	@Override
                    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        				if (ack) {
        	                log.info("消息投递到交换机成功:[correlationData={}]",correlationData);
        	            } else {
        	                log.error("消息投递到交换机失败:[correlationData={},原因:{}]", correlationData, cause);
        	            }
                    }
                });
            }
        }
        
      • 使用ReturnCallback Broker/Exchange-->Broker/Queue回调

        和注册confirmCallback的原理一样,就不多赘述,直接看配置,需要注意的是 这个回调只会在消息在从Exchange投递到Queue【失败】时被执行

        package com.bike.rabbitmq.service;
        
        import com.alibaba.fastjson.JSONObject;
        import lombok.extern.slf4j.Slf4j;
        import org.springframework.amqp.core.ReturnedMessage;
        import org.springframework.amqp.rabbit.core.RabbitTemplate;
        import org.springframework.stereotype.Component;
        
        @Slf4j
        @Component
        public class ReturnsCallBackService implements RabbitTemplate.ReturnsCallback {
            @Override
            public void returnedMessage(ReturnedMessage returnedMessage) {
                /**
                 * message;消息体
                 * replyCode;响应code
                 * replyText;响应内容
                 * exchange;交换机
                 * routingKey;路由键
                 */
                log.info("returns call back info : {}", JSONObject.toJSONString(returnedMessage));
            }
        }
        
        
    5. 消费者

      package com.bike.rabbitmq.controller;
      
      import com.rabbitmq.client.Channel;
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.amqp.core.Message;
      import org.springframework.amqp.rabbit.annotation.RabbitHandler;
      import org.springframework.amqp.rabbit.annotation.RabbitListener;
      import org.springframework.stereotype.Component;
      
      import java.io.IOException;
      
      @Component
      @RabbitListener(queues = "consumer_route")
      @Slf4j
      public class ConsumerService {
      
          @RabbitHandler
          public void consumer(String msg, Message message, Channel channel) throws Exception{
              long deliveryTag = message.getMessageProperties().getDeliveryTag();
              try {
                  /**
                   * deliveryTag:表示消息投递序号,每次消费消息或者消息重新投递后,deliveryTag都会增加。手动消息确认模式下,我们可以对指定deliveryTag的消息进行ack、nack、reject等操作。
                   *
                   * multiple:是否批量确认,值为true则会一次性ack所有小于当前消息deliveryTag的消息。
                   */
      
                  log.info("consume message content  : {}", msg);
                  channel.basicAck(deliveryTag, false);
              } catch (IOException e) {
                  if (message.getMessageProperties().getRedelivered()){
                      /**
                       * basicReject:拒绝消息,与basicNack区别在于不能进行批量操作,其他用法很相似。
                       * deliveryTag:表示消息投递序号。
                       *
                       * requeue:值为 true 消息将重新入队列。
                       */
                      log.error("消息重复处理失败,拒绝处理");
                      channel.basicReject(deliveryTag,false);
                  }else {
                      /**
                       * basicNack :表示失败确认,一般在消费消息业务异常时用到此方法,可以将消息重新投递入队列。
                       * deliveryTag:表示消息投递序号。
                       *
                       * multiple:是否批量确认。
                       *
                       * requeue:值为 true 消息将重新入队列。
                       * 对于重复消费异常的数据可使用channel.basicPublish();方法放在消费队列的末端,使用channel.basicNack(deliveryTag,false,true);该方法
                       * 会一直重复不停的消费,导致死循环,所以对于异常数据可先确认收到,然后重新发布到队列末端,还可以记录重复消费次数,超过上限可做持久化数据处理并推送报警,人工干预
                       在redis或者数据库中记录重试次数,达到最大重试次数以后消息进入死信队列或者其他队列,再单独针对这些消息进行处理;
      
      使用spring-rabbit中自带的retry功能;
                       */
      
                      log.error("消息即将返回队列。。");
                      channel.basicNack(deliveryTag,false,true);
                  }
              }
          }
      
      }
      
      
    6. rabbitmq重试机制

      • 对于消息消费失败的情况下可以使用rabbitmq自带的retry功能,具体参见配置文件

      • 在redis或者数据库记录重试次数,达到最大值进行人工干预,或者进入其他队列,对消息进行单独处理

      • 注意:

        重试并不是RabbitMQ重新发送了消息,仅仅是消费者内部进行的重试,换句话说就是重试跟mq没有任何关系;

        因此上述消费者代码不能添加try{}catch(){},一旦捕获了异常,在自动ack模式下,就相当于消息正确处理了,消息直接被确认掉了,不会触发重试的;

    7. 消费异常的消息处理机制

      • 重试机制的重要接口类MessageRecoverer其实现类有ImmediateRequeueMessageRecoverer和RejectAndDontRequeueRecoverer、RepublishMessageRecoverer

        ![image-20210608140612685](/Users/xiuxian/Library/Application Support/typora-user-images/image-20210608140612685.png)

        1. 默认使用的是RejectAndDontRequeueRecoverer实现类,根据实现类的名字我们就可以看出来该实现类的作用就是拒绝并且不会将消息重新发回队列。

        2. RepublishMessageRecoverer重新发布消息

          @Bean
          public DirectExchange errorExchange(){
          	return new DirectExchange("error-exchange",true,false);
          }
          
          @Bean
          public Queue errorQueue(){
          	return new Queue("error-queue", true);
          }
          
          @Bean
          public Binding errorBinding(Queue errorQueue, DirectExchange errorExchange){
          	return BindingBuilder.bind(errorQueue).to(errorExchange).with("error-routing-key");
          }
          
          //创建RepublishMessageRecoverer,对于重试以后异常的消息会重新发布到异常队列里面
          @Bean
          public MessageRecoverer messageRecoverer(){
          	return new RepublishMessageRecoverer(rabbitTemplate,"error-exchange","error-routing-key");
          }
          
          
          
        3. ImmediateRequeueMessageRecoverer立即重新返回队列

          @Bean
          public MessageRecoverer messageRecoverer(){
          	return new ImmediateRequeueMessageRecoverer();
          }
          

          会立即返回队列一直往复的循环重试,由于异常并不是马上能解决,所以会影响到后续消息的消费,不建议该方式处理

        4. 死信队列

          /**
           * 死信交换机
           * @return
           */
          @Bean
          public DirectExchange dlxExchange(){
          	return new DirectExchange(dlxExchangeName);
          }
          
          /**
           * 死信队列
           * @return
           */
          @Bean
          public Queue dlxQueue(){
          	return new Queue(dlxQueueName);
          }
          
          /**
           * 死信队列绑定死信交换机
           * @param dlxQueue
           * @param dlxExchange
           * @return
           */
          @Bean
          public Binding dlcBinding(Queue dlxQueue, DirectExchange dlxExchange){
          	return BindingBuilder.bind(dlxQueue).to(dlxExchange).with(dlxRoutingKey);
          }
          
          
          //业务队列的创建需要做一些修改,添加死信交换机以及死信路由键的配置
          /**
           * 业务队列
           * @return
           */
          @Bean
          public Queue queue(){
          	Map<String,Object> params = new HashMap<>();
          	params.put("x-dead-letter-exchange",dlxExchangeName);//声明当前队列绑定的死信交换机
          	params.put("x-dead-letter-routing-key",dlxRoutingKey);//声明当前队列的死信路由键
          	return QueueBuilder.durable(queueName).withArguments(params).build();
          }
          

          此时启动服务会发现业务队列和私信队列

          img

          在业务队列上出现了DLX以及DLK的标识,标识已经绑定了死信交换机以及死信路由键,此时调用生产者发送消息,消费者在重试5次后,由于MessageCover默认的实现类是RejectAndDontRequeueRecoverer,也就是requeue=false,又因为业务队列绑定了死信队列,因此消息会从业务队列中删除,同时发送到死信队列中。

          注意:

          如果ack模式是手动ack,那么需要调用channe.nack方法,同时设置requeue=false才会将异常消息发送到死信队

        ​ 源码地址:源码地址