MQ学习

一丶消息队列

MQ全称为Message Queue,即消息队列。“消息队列”是在消息的传输过程中保存消息的容器。它是典型的:生产者、消费者模型。生产者不断向消息队列中生产消息,消费者不断的从队列中获取消息。因为消息的生产和消费都是异步的,而且只关心消息的发送和接收,没有业务逻辑的侵入,这样就实现了生产者和消费者的解耦。

二丶为什么要使用MQ

1丶系统解耦

如图:

系统未解耦前

RabbitMQ学习及SpringBoot集成RabbitMQ_参数设置

系统解耦后

RabbitMQ学习及SpringBoot集成RabbitMQ_参数设置_02

2丶异步调用

异步调用前

RabbitMQ学习及SpringBoot集成RabbitMQ_spring_03

异步调用后

RabbitMQ学习及SpringBoot集成RabbitMQ_spring_04

3丶流量削峰

流量削峰前

RabbitMQ学习及SpringBoot集成RabbitMQ_参数设置_05

 

流量削峰后

RabbitMQ学习及SpringBoot集成RabbitMQ_spring_06

 

三丶项目引入MQ的缺点

1丶系统可用性降低:

系统引入的外部依赖越多,系统要面对的风险越高,拿场景一来说,本来ABCD四个系统配合的好好的,没啥问题,但是你偏要弄个MQ进来插一脚,虽然好处挺多,但是万一MQ挂掉了呢,那样你系统不也就挂掉了。

2丶系统复杂程度提高:

非要加个MQ进来,如何保证没有重复消费呢?如何处理消息丢失的情况?怎么保证消息传递的顺序?问题太多

3丶一致性的问题:

A系统处理完再传递给MQ就直接返回成功了,用户以为你这个请求成功了,但是,如果在BCD的系统里,BC两个系统写库成功,D系统写库失败了怎么办,这样就导致数据不一致了。

所以。消息队列其实是一套非常复杂的架构,你在享受MQ带来的好处的同时,也要做各种技术方案把MQ带来的一系列的问题解决掉,等一切都做好之后,系统的复杂程度硬生生提高了一个等级。

 

RabbitMQ学习

一丶RabbitMQ流程图

RabbitMQ学习及SpringBoot集成RabbitMQ_ide_07

组成部分说明

  • Broker:消息队列服务进程,此进程包括两个部分:Exchange和Queue

  • Exchange:消息队列交换机,按一定的规则将消息路由转发到某个队列,对消息进行过虑。

  • Queue:消息队列,存储消息的队列,消息到达队列并转发给指定的

  • Producer:消息生产者,即生产方客户端,生产方客户端将消息发送

  • Consumer:消息消费者,即消费方客户端,接收MQ转发的消息。

生产者发送消息流程:

  1. 生产者和Broker建立TCP连接。

  1. 生产者和Broker建立通道。

  1. 生产者通过通道消息发送给Broker,由Exchange将消息进行转发。

  1. Exchange将消息转发到指定的Queue(队列)

消费者接收消息流程:

  1. 消费者和Broker建立TCP连接

  1. 消费者和Broker建立通道

  1. 消费者监听指定的Queue(队列)

  1. 当有消息到达Queue时Broker默认将消息推送给消费者。

  1. 消费者接收到消息。

  1. ack回复

 

二丶六种消息模型

1丶基本消息模型:

RabbitMQ学习及SpringBoot集成RabbitMQ_ide_08

  • P:生产者,也就是要发送消息的程序

  • C:消费者:消息的接受者,会一直等待消息到来。

  • queue:消息队列,图中红色部分。可以缓存消息;生产者向其中投递消息,消费者从其中取出消息。

生产者
  1. 新建一个maven工程,添加amqp-client依赖

<!-- rabbitmq依赖 -->
<dependency>
  <groupId>com.rabbitmq</groupId>
  <artifactId>amqp-client</artifactId>
  <version>5.7.1</version>
</dependency>
<!-- spring的核心依赖,用于读取配置文件 -->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context</artifactId>
  <version>5.0.6.RELEASE</version>
</dependency>
  1. 连接工具类

  • 配置文件

rabbitmqConfig.properties

## rabbitmq配置文件
host=localhost
port=5672
virtualHost=/
username=guest
password=guest
  • 读取配置文件工具类

CommonUtil

public class CommonUtil {
  /**
    * 获取resources下的propertes文件,读取配置
    *
    * @author Wcj
    * @date: 2021/7/9 10:59
    */
  public static Map<String, String> getProperties() throws UnsupportedEncodingException {

      try {
          Properties pro = PropertiesLoaderUtils.loadAllProperties("rabbitmqConfig.properties");
          Map<String, String> result = new HashMap<>();

          Set<Map.Entry<Object, Object>> entries = pro.entrySet();

          for (Map.Entry<Object, Object> entry : entries) {

              String key =(String) entry.getKey();
              String value =(String)entry.getValue();
              value = URLEncoder.encode(value, "ISO-8859-1");
              value = URLDecoder.decode(value, "GBK");
              result.put(key,value);
          }
          return result;
      } catch (FileNotFoundException e) {
          e.printStackTrace();
      } catch (IOException e) {
          e.printStackTrace();
      }

      return null;
  }

}
  • rabbitmq连接工具类

public class ConnectionUtil {


  /**
    * 获取rabbitmq连接
    * @author Wcj
    * @date: 2021/7/9 16:13
    */
  public static Connection getConnection() throws IOException, TimeoutException {
      ConnectionFactory factory = new ConnectionFactory();
      Map<String, String> properties = CommonUtil.getProperties();
      String host = properties.get("host");
      String port = properties.get("port");
      String virtualHost = properties.get("virtualHost");
      String username = properties.get("username");
      String password = properties.get("password");
      factory.setHost(host);
      //端口
      factory.setPort(Integer.parseInt(port));
      //设置账号信息,用户名、密码、vhost
      factory.setVirtualHost(virtualHost);//设置虚拟机,一个mq服务可以设置多个虚拟机,每个虚拟机就相当于一个独立的mq
      factory.setUsername(username);
      factory.setPassword(password);

      Connection connection = factory.newConnection();
      return connection;
  }
}
  • 生产者发送消息

public class SimpleProducer {

  private static final String SIMPLE_FILE_NAME = "simpleQueue";


  public static void main(String[] args) throws IOException, TimeoutException {
      try(// 获取连接
          Connection connection = ConnectionUtil.getConnection();

          // 从连接中获取通道
          Channel channel = connection.createChannel();){
          // 3、声明(创建)队列
          //参数:String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
          /**
            * 参数明细
            * 1、queue 队列名称
            * 2、durable 是否持久化,如果持久化,mq重启后队列还在
            * 3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
            * 4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
            * 5、arguments 参数,可以设置一个队列的扩展参数,比如:可设置存活时间
            */
          channel.queueDeclare(SIMPLE_FILE_NAME,false,false,false,null);
          String message = "简单消息模式发送消息内容:RabbitMQ冲冲冲!!!======>";
          System.out.println("生产者生产了消息:"+ message);
          // 向指定的队列中发送消息
          //参数:String exchange, String routingKey, BasicProperties props, byte[] body
          /**
            * 参数明细:
            * 1、exchange,交换机,如果不指定将使用mq的默认交换机(设置为"")
            * 2、routingKey,路由key,交换机根据路由key来将消息转发到指定的队列,如果使用默认交换机,routingKey设置为队列的名称
            * 3、props,消息的属性
            * 4、body,消息内容
            */
          channel.basicPublish("",SIMPLE_FILE_NAME,null,message.getBytes(StandardCharsets.UTF_8));
      }
  }
}

 

消费者
/**
* @author Wcj
* @description: 简单消息模式消费者一
* 正常消费消息,消息自动ACK,消费消息正常
* @date 2021/7/9 16:03
*/
public class SimpleConsumerOne {

  private static final String SIMPLE_FILE_NAME = "simpleQueue";

  public static void main(String[] args) throws IOException, TimeoutException {
      Connection connection = ConnectionUtil.getConnection();
      Channel channel = connection.createChannel();
      // 声明队列
      //参数:String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
      /**
        * 参数明细
        * 1、queue 队列名称
        * 2、durable 是否持久化,如果持久化,mq重启后队列还在
        * 3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
        * 4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
        * 5、arguments 参数,可以设置一个队列的扩展参数,比如:可设置存活时间
        */
      channel.queueDeclare(SIMPLE_FILE_NAME, false, false, false, null);
      // 实现消费的方法
      DefaultConsumer consumer = new DefaultConsumer(channel) {
          @Override
          public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
              //交换机
              String exchange = envelope.getExchange();
              System.out.println("交换机名称为:" + exchange);
              //消息id,mq在channel中用来标识消息的id,可用于确认消息已接收
              long deliveryTag = envelope.getDeliveryTag();
              System.out.println("消息id为:" + deliveryTag);
              // body 即消息体
              String msg = new String(body, StandardCharsets.UTF_8);
              System.out.println("消息内容为:" + msg);
          }
      };
      // 监听队列,第二个参数:是否自动进行消息确认。
      //参数:String queue, boolean autoAck, Consumer callback
      /**
        * 参数明细:
        * 1、queue 队列名称
        * 2、autoAck 自动回复,当消费者接收到消息后要告诉mq消息已接收,如果将此参数设置为tru表示会自动回复mq,如果设置为false要通过编程实现回复
        * 3、callback,消费方法,当消费者接收到消息要执行的方法
        */
      channel.basicConsume(SIMPLE_FILE_NAME, true, consumer);


  }
}

 

演示消费者发送异常的时候消费者怎么处理
/**
* @author Wcj
* @description: 简单消息模式消费者二
* 消费者发生异常,手动进行消息ACk,消息未消费
* @date 2021/7/9 16:03
*/
public class SimpleConsumerTwo {

  private static final String SIMPLE_FILE_NAME = "simpleQueue";

  public static void main(String[] args) throws IOException, TimeoutException {

      Connection connection = ConnectionUtil.getConnection();
      Channel channel = connection.createChannel();
// 声明队列
      //参数:String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
      /**
        * 参数明细
        * 1、queue 队列名称
        * 2、durable 是否持久化,如果持久化,mq重启后队列还在
        * 3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
        * 4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
        * 5、arguments 参数,可以设置一个队列的扩展参数,比如:可设置存活时间
        */
      channel.queueDeclare(SIMPLE_FILE_NAME, false, false, false, null);
      // 实现消费的方法
      DefaultConsumer consumer = new DefaultConsumer(channel) {
          @Override
          public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
              // 演示消费者消息接受失败,消息不会被消费
              int i = 1 / 0;
              //交换机
              String exchange = envelope.getExchange();
              System.out.println("交换机名称为:{}" + exchange);
              //消息id,mq在channel中用来标识消息的id,可用于确认消息已接收
              long deliveryTag = envelope.getDeliveryTag();
              System.out.println("消息id为:{}" + deliveryTag);
              // body 即消息体
              String msg = new String(body, StandardCharsets.UTF_8);
              System.out.println("消息内容为:{}" + msg);
              // 手动进行ACK
              channel.basicAck(deliveryTag, false);
          }
      };
      // 监听队列,第二个参数:是否自动进行消息确认。
      //参数:String queue, boolean autoAck, Consumer callback
      /**
        * 参数明细:
        * 1、queue 队列名称
        * 2、autoAck 自动回复,当消费者接收到消息后要告诉mq消息已接收,如果将此参数设置为tru表示会自动回复mq,如果设置为false要通过编程实现回复
        * 3、callback,消费方法,当消费者接收到消息要执行的方法
        */
      channel.basicConsume(SIMPLE_FILE_NAME, false, consumer);


  }
}

 

2丶Work消息模型

RabbitMQ学习及SpringBoot集成RabbitMQ_发送消息_09

work queues与入门程序相比,多了一个消费端,两个消费端共同消费同一个队列中的消息,但是一个消息只能被一个消费者获取。

这个消息模型在Web应用程序中特别有用,可以处理短的HTTP请求窗口中无法处理复杂的任务。

接下来我们来模拟这个流程:

P:生产者:任务的发布者

C1:消费者1:领取任务并且完成任务,假设完成速度较慢(模拟耗时)

C2:消费者2:领取任务并且完成任务,假设完成速度较快

生产者
/**
* @author Wcj
* @description:竞争工作模式生产者
* @date 2021/7/12 10:46
*/
public class ContendWordProducer {
  // 循环生产50条数据
  private static final String WORK_QUEUE = "workQueue";

  public static void main(String[] args) throws IOException, TimeoutException {
      Connection connection = ConnectionUtil.getConnection();
      Channel channel = connection.createChannel();
      channel.queueDeclare(WORK_QUEUE,false,false,false,null);
      // 循环发送五十条信息
      for (int i = 1; i <= 50; i++) {
          StringBuilder builder = new StringBuilder("竞争工作队列消息");
          builder.append(i);
          String message = builder.toString();
          channel.basicPublish("",WORK_QUEUE,null,message.getBytes(StandardCharsets.UTF_8));
      }

  }
}

 

消费者

通过 BasicQos 方法设置prefetchCount = 1。这样RabbitMQ就会使得每个Consumer在同一个时间点最多处理1个Message。换句话说,在接收到该Consumer的ack前,他它不会将新的Message分发给它。相反,它会将其分派给不是仍然忙碌的下一个Consumer。

值得注意的是:prefetchCount在手动ack的情况下才生效,自动ack不生效。

package com.zzzwww.rabbitmq.contendwordmode;

import com.rabbitmq.client.*;
import com.zzzwww.utils.ConnectionUtil;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;

/**
* @author Wcj
* @description: 竞争工作消息模式消费者一
* 正常消费消息,消息自动ACK,消费消息正常
* @date 2021/7/9 16:03
*/
public class ContennnndWordConsumerOne {

  private static final String WORK_QUEUE = "workQueue";


  public static void main(String[] args) throws IOException, TimeoutException {
      Connection connection = ConnectionUtil.getConnection();
      Channel channel = connection.createChannel();
      // 声明队列
      //参数:String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
      /**
        * 参数明细
        * 1、queue 队列名称
        * 2、durable 是否持久化,如果持久化,mq重启后队列还在
        * 3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
        * 4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
        * 5、arguments 参数,可以设置一个队列的扩展参数,比如:可设置存活时间
        */
      channel.queueDeclare(WORK_QUEUE, false, false, false, null);
      // 设置每个消费者同时只能处理一条消息,在手动ACK下才生效
      channel.basicQos(1);
      // 实现消费的方法
      DefaultConsumer consumer = new DefaultConsumer(channel) {
          @Override
          public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
              //交换机
              String exchange = envelope.getExchange();
              //消息id,mq在channel中用来标识消息的id,可用于确认消息已接收
              long deliveryTag = envelope.getDeliveryTag();
              // body 即消息体
              String msg = new String(body, StandardCharsets.UTF_8);
              System.out.println("消息内容为:" + msg);
              channel.basicAck(deliveryTag,false);
          }
      };
      // 监听队列,第二个参数:是否自动进行消息确认。
      //参数:String queue, boolean autoAck, Consumer callback
      /**
        * 参数明细:
        * 1、queue 队列名称
        * 2、autoAck 自动回复,当消费者接收到消息后要告诉mq消息已接收,如果将此参数设置为tru表示会自动回复mq,如果设置为false要通过编程实现回复
        * 3、callback,消费方法,当消费者接收到消息要执行的方法
        */
      channel.basicConsume(WORK_QUEUE, false, consumer);


  }
}

 

订阅模型分类

1、一个生产者多个消费者 2、每个消费者都有一个自己的队列 3、生产者没有将消息直接发送给队列,而是发送给exchange(交换机、转发器) 4、每个队列都需要绑定到交换机上 5、生产者发送的消息,经过交换机到达队列,实现一个消息被多个消费者消费 例子:注册->发邮件、发短信

X(Exchanges):交换机一方面:接收生产者发送的消息。另一方面:知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。

Exchange类型有以下几种:

Fanout:广播,将消息交给所有绑定到交换机的队列

Direct:定向,把消息交给符合指定routing key 的队列

Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列

Header:header模式与routing不同的地方在于,header模式取消routingkey,使用header中的 key/value(键值对)匹配队列。

 

3丶Publish/subscribe

RabbitMQ学习及SpringBoot集成RabbitMQ_持久化_10

(交换机类型:Fanout,也称为广播

生产者
  • 生产者不需要声明queue

  • 生产者声明Exchange,消息直接发送到Exchange

/**
* @author Wcj
* @description: 广播类型的生产者
* @date 2021/7/12 15:54
*/
public class FanoutExchangeProducer {

  private static final String FANOUT_EXCHANGE = "fanoutExchange";

  public static void main(String[] args) throws IOException, TimeoutException {
      // 获取连接
      Connection connection = ConnectionUtil.getConnection();
      Channel channel = connection.createChannel();
      // 声明交换机,指定类型为fanout
      channel.exchangeDeclare(FANOUT_EXCHANGE, BuiltinExchangeType.FANOUT);
      String message = "广播模式发送消息,冲冲冲!!!=======>";
      /**
        * 交换机名称
        * 路由键
        * 消息的属性
        * 消息的内容
        */
      channel.basicPublish(FANOUT_EXCHANGE,"",null,message.getBytes());
      System.out.println(message);
      channel.close();
      connection.close();
  }

}

 

消费者
/**
* @author Wcj
* @description: 广播类型的消费者
* @date 2021/7/12 16:19
*/
public class FanoutExchangeConsumer {
  private static final String FANOUT_EXCHANGE = "fanoutExchange";

  private static final String FANOUT_QUEUE_ONE = "faoutQueueOne";

  public static void main(String[] args) throws IOException, TimeoutException {
      // 获取连接
      Connection connection = ConnectionUtil.getConnection();
      Channel channel = connection.createChannel();
      // 声明队列
      channel.queueDeclare(FANOUT_QUEUE_ONE,false,false,false,null);
      // 绑定队列交换机
      channel.queueBind(FANOUT_QUEUE_ONE,FANOUT_EXCHANGE,"");
      // 定义队列的消费者
      DefaultConsumer consumer = new DefaultConsumer(channel) {
          // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
          @Override
          public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
              String msg = new String(body, StandardCharsets.UTF_8);
              System.out.println("消息内容为:" + msg);
          }
      };
      // 监听队列,自动返回完成
      channel.basicConsume(FANOUT_QUEUE_ONE,true,consumer);
  }

}

 

4丶Routing 路由模型

交换机类型:direct,也叫直连型交换机

RabbitMQ学习及SpringBoot集成RabbitMQ_持久化_11

P:生产者,向Exchange发送消息,发送消息时,会指定一个routing key。

X:Exchange(交换机),接收生产者的消息,然后把消息递交给 与routing key完全匹配的队列

C1:消费者,其所在队列指定了需要routing key 为 sms的消息

生产者
/**
* @author Wcj
* @description: routing模式消息生产者
* @date 2021/7/14 14:42
*/
public class RoutingExchangeProducer {

  private static final String ROUTING_EXCHANGE_NAME = "routing_exchange_name";

  private static final String ROUTING_KEY = "sms";

  public static void main(String[] args) throws IOException, TimeoutException {
      // 获取连接
      Connection connection = ConnectionUtil.getConnection();
      // 获取通道
      Channel channel = connection.createChannel();
      // 声明交换机
      channel.exchangeDeclare(ROUTING_EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
      String message = "routing模式发送消息冲冲冲!!!====>";
      // 发送消息指定routingkey为sms的routingkey才能接收到信息
      channel.basicPublish(ROUTING_EXCHANGE_NAME,ROUTING_KEY,null,message.getBytes());
      System.out.println(message);
      channel.close();;
      connection.close();
  }
}

 

消费者一
/**
* @author Wcj
* @description: routing模式消息消费者一
* @date 2021/7/14 15:00
*/
public class RoutingExchangeConsumerOne {

  private static final String ROUTING_EXCHANGE_NAME = "routing_exchange_name";

  private static final String ROUTING_QUEUE_NAME = "routing_queue_name";

  private static final String ROUTING_KEY = "sms";

  public static void main(String[] args) throws IOException, TimeoutException {
      // 获取连接
      Connection connection = ConnectionUtil.getConnection();
      // 获取通道
      Channel channel = connection.createChannel();
      // 声明队列
      channel.queueDeclare(ROUTING_QUEUE_NAME,false,false,false,null);
      // 队列绑定交换机指定routingkey
      channel.queueBind(ROUTING_QUEUE_NAME,ROUTING_EXCHANGE_NAME,ROUTING_KEY);
      DefaultConsumer consumer = new DefaultConsumer(channel) {
          @Override
          public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
              String message = new String(body);
              System.out.println(message);
          }
      };
      // 监听队列,自动ACK
      channel.basicConsume(ROUTING_QUEUE_NAME,true,consumer);
  }
}
消费者二
/**
* @author Wcj
* @description: routing模式消息消费者一
* @date 2021/7/14 15:00
*/
public class RoutingExchangeConsumerTwo {

  private static final String ROUTING_EXCHANGE_NAME = "routing_exchange_name";

  private static final String ROUTING_QUEUE_NAME = "routing_queue_name";

  private static final String ROUTING_KEY = "email";

  public static void main(String[] args) throws IOException, TimeoutException {
      // 获取连接
      Connection connection = ConnectionUtil.getConnection();
      // 获取通道
      Channel channel = connection.createChannel();
      // 声明队列
      channel.queueDeclare(ROUTING_QUEUE_NAME,false,false,false,null);
      // 队列绑定交换机指定routingkey
      channel.queueBind(ROUTING_QUEUE_NAME,ROUTING_EXCHANGE_NAME,ROUTING_KEY);
      DefaultConsumer consumer = new DefaultConsumer(channel) {
          @Override
          public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
              String message = new String(body);
              System.out.println(message);
          }
      };
      // 监听队列,自动ACK
      channel.basicConsume(ROUTING_QUEUE_NAME,true,consumer);
  }
}

只有消费者一才能收到消息,因为消费者一是监听Routingkey为sms的队列

 

5丶Topics 通配符模式

交换机类型:topics

RabbitMQ学习及SpringBoot集成RabbitMQ_spring_12

  • 每个消费者监听自己的队列,并且设置带统配符的routingkey,生产者将消息发给broker,由交换机根据routingkey来转发消息到指定的队列。

  • Routingkey一般都是有一个或者多个单词组成,多个单词之间以“.”分割,例如:topic.man

通配符规则:

  • #:匹配一个或多个词

  • *:匹配不多不少恰好1个词

 

生产者
/**
* @author Wcj
* @description: Topic通配符模式消息生产者
* @date 2021/7/14 15:36
*/
public class TopicExchangeProducer {
// Routingkey一般都是有一个或者多个单词组成,多个单词之间以“.”分割,例如:inform.sms
// 通配符规则:
// #:匹配一个或多个词
// *:匹配不多不少恰好1个词

  private static final String TOPIC_EXCHANGE_NAME = "topic_exchange_name";

  private static final String ROUTING_KEY = "zw.sms";

  public static void main(String[] args) throws IOException, TimeoutException {
      Connection connection = ConnectionUtil.getConnection();
      Channel channel = connection.createChannel();
      channel.exchangeDeclare(TOPIC_EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
      String message = "这是给zw系统发送的sms消息===>";
      channel.basicPublish(TOPIC_EXCHANGE_NAME,ROUTING_KEY,null,message.getBytes());
      System.out.println(message);
      channel.close();
      connection.close();
  }
}

 

消费者一
/**
* @author Wcj
* @description: Topic通配符模式消息消费者一
* 监听zw下的服务
* @date 2021/7/14 15:36
*/
public class TopicExchangeConsumerOne {
// Routingkey一般都是有一个或者多个单词组成,多个单词之间以“.”分割,例如:inform.sms
// 通配符规则:
// #:匹配一个或多个词
// *:匹配不多不少恰好1个词

  private static final String TOPIC_EXCHANGE_NAME = "topic_exchange_name";

  private static final String TOPIC_QUEUE_NAME = "topic_queue_name";

  private static final String ROUTING_KEY = "zw.*";

  public static void main(String[] args) throws IOException, TimeoutException {
      Connection connection = ConnectionUtil.getConnection();
      Channel channel = connection.createChannel();
      channel.queueDeclare(TOPIC_QUEUE_NAME,false,false,false,null);
      channel.queueBind(TOPIC_QUEUE_NAME,TOPIC_EXCHANGE_NAME,ROUTING_KEY);
      // 定义队列的消费者
      DefaultConsumer consumer = new DefaultConsumer(channel) {
          // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
          @Override
          public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
              String msg = new String(body, StandardCharsets.UTF_8);
              System.out.println("消息内容为:" + msg);
          }
      };
      channel.basicConsume(TOPIC_QUEUE_NAME,true,consumer);
  }
}

 

消费者二
/**
* @author Wcj
* @description: Topic通配符模式消息消费者二
* 监听sms的消息
* @date 2021/7/14 15:36
*/
public class TopicExchangeConsumerTwo {
// Routingkey一般都是有一个或者多个单词组成,多个单词之间以“.”分割,例如:inform.sms
// 通配符规则:
// #:匹配一个或多个词
// *:匹配不多不少恰好1个词

  private static final String TOPIC_EXCHANGE_NAME = "topic_exchange_name";

  private static final String TOPIC_QUEUE_NAME = "topic_queue_name";

  private static final String ROUTING_KEY = "*.sms";

  public static void main(String[] args) throws IOException, TimeoutException {
      Connection connection = ConnectionUtil.getConnection();
      Channel channel = connection.createChannel();
      channel.queueDeclare(TOPIC_QUEUE_NAME,false,false,false,null);
      channel.queueBind(TOPIC_QUEUE_NAME,TOPIC_EXCHANGE_NAME,ROUTING_KEY);
      // 定义队列的消费者
      DefaultConsumer consumer = new DefaultConsumer(channel) {
          // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
          @Override
          public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
              String msg = new String(body, StandardCharsets.UTF_8);
              System.out.println("消息内容为:" + msg);
          }
      };
      channel.basicConsume(TOPIC_QUEUE_NAME,true,consumer);
  }
}

两个消费者都能监听到消息

 

 

SpringBoot集成RabbitMQ学习

三种常见交换机模型

  • Direct Exchange

直连型交换机,根据消息携带的路由键将消息投递给对应队列。

大致流程,有一个队列绑定到一个直连交换机上,同时赋予一个路由键 routing key 。 然后当一个消息携带着路由值为X,这个消息通过生产者发送给交换机时,交换机就会根据这个路由值X去寻找绑定值也是X的队列。

  • Fanout Exchange

扇型交换机,这个交换机没有路由键概念,就算你绑了路由键也是无视的。 这个交换机在接收到消息后,会直接转发到绑定到它上面的所有队列。

  • Topic Exchange

主题交换机,这个交换机其实跟直连交换机流程差不多,但是它的特点就是在它的路由键和绑定键之间是有规则的。 简单地介绍下规则: * (星号) 用来表示一个单词 (必须出现的) # (井号) 用来表示任意数量(零个或多个)单词 通配的绑定键是跟队列进行绑定的

 

一丶项目准备

  • 创建两个项目,一个生产者springboot-rabbitmq-producer,一个消费者spring-rabbitmq-consumer-

  • 引入依赖

<parent>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-parent</artifactId>
       <version>2.5.2</version>
       <relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
   <java.version>1.8</java.version>
</properties>
<dependencies>
   <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-web</artifactId>
   </dependency>

   <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-test</artifactId>
       <scope>test</scope>
   </dependency>

   <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-amqp</artifactId>
   </dependency>
</dependencies>
  • 配置文件

server:
port: 10096
spring:
application:
  name: mq-rabbitmq-producer
 # 配置rabbirmq服务器
rabbitmq:
  host: localhost
  port: 5672
  username: root
  password: root
   # 虚拟host,可以不设置,使用server默认host
  virtualHost: /zzzwww
  template:
    retry:
      enabled: true
      initial-interval: 10000ms
      max-interval: 300000ms
      multiplier: 2

消费者配置文件只有端口不同

 

二丶Direct(直连型交换机)交换机模型

固定RoutingKey

当有多个消费者监听队列时,消息会轮询消费,不会出现消息重复消费现象

生产者

1. 配置类

声明交换机,声明队列,队列绑定交换机声明RoutingKey

/**
* @author Wcj
* @description: rabbitmq直连型交换机配置文件
* @date 2021/7/15 10:59
*/
@Configuration
public class DirectRabbitConfig {

   // 直连型队列名称
   public static final String QUEUE_NAME = "direct_queue";
   // 直连型交换机名称
   public static final String EXCHANGE_NAME = "direct_exchange";
   // 直连交换机的routingkey
   public static final String ROUTING_KEY = "direct_routing_key";

   /**
    * 声明队列
    * @author Wcj
    * @date: 2021/7/15 11:08
    */
   @Bean()
   public Queue DirectQueue(){
       // durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
       // exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
       // autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。
       // return new Queue("TestDirectQueue",true,true,false);
       // 一般设置一下队列的持久化就好,其余两个就是默认false
       return new Queue(QUEUE_NAME,true);
  }

   /**
    * 声明交换机
    * @author Wcj
    * @date: 2021/7/15 11:11
    */
   @Bean
   public DirectExchange DirectExchange(){
       return new DirectExchange(EXCHANGE_NAME,true,false);
  }

   /**
    * 将队列和交换机绑定,并设置匹配键:direct_routing_key
    * @author Wcj
    * @date: 2021/7/15 11:15
    */
   @Bean
   public Binding bindDirect(){
       return BindingBuilder.bind(DirectQueue()).to(DirectExchange()).with(ROUTING_KEY);
  }

}
2. 发送消息

使用springboot提供的RabbitTemplate发送消息

@Autowired private RabbitTemplate rabbitTemplate;

    
   /**
    * 直连型交换机发送消息
    *
    * @author Wcj
    * @date: 2021/7/15 16:38
    */
   @GetMapping("/directMessage")
   public String sendDirectMessage() {
       String messageId = String.valueOf(UUID.randomUUID());
       String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
       // 将消息携带绑定键值:direct_routing_key,发送到交换机:EXCHANGE_NAME

       Map<String, String> map = new HashMap<>();
       map.put("messageId", messageId);
       map.put("createTime", createTime);
       String message = "直连类型的交换机模式消息";
       map.put("messageData", message);
       rabbitTemplate.convertAndSend(DirectRabbitConfig.EXCHANGE_NAME, DirectRabbitConfig.ROUTING_KEY, map);
       return "success";
  }

 

消费者

创建RabbitMq监听类

/**
* @author Wcj
* @description: 监听直连型模式消息的监听类
* @date 2021/7/15 14:35
*/
@Component
@RabbitListener(queues = "direct_queue")
public class DirectReceiverOne {


   @RabbitHandler
   public void process(Map message){
       System.out.println("DirectReceiverOne消费者收到消息:"+message.toString());
  }
}

 

三丶Topic交换机模型

匹配RoutingKey规则的交换机

生产者

1.配置类

声明交换机,声明队列,队列绑定交换机声明RoutingKey

/**
* @author Wcj
* @description: Topic交换机配置类
* @date 2021/7/15 15:05
*/
@Configuration
public class TopicRabbitConfig {
   // routingkey
   public static final String MAN = "topic.man";
   // routingkey
   public static final String PERSON = "topic.*";

   public static final String TOPIC_QUEUE_MAN = "topic_queue_man";

   public static final String TOPIC_QUEUE = "topic_queue";

   public static final String TOPIC_EXCHANGE = "topic_exchange";

   // 声明队列
   @Bean
   public Queue topicManQueue(){
       return new Queue(TOPIC_QUEUE_MAN);
  }

   // 声明队列
   @Bean
   public Queue topicQueue(){
       return new Queue(TOPIC_QUEUE);
  }


   // 声明交换机
   @Bean
   public Exchange topicExchange(){
       return new TopicExchange(TOPIC_EXCHANGE);
  }

   // topic_queue队列绑定topic_exchange交换机,绑定的键为topic.man
   // 这样只要是消息携带的路由键是topic.woman,才会分发到该队列
   @Bean
   public Binding bindingExchangeMessage(){
       return BindingBuilder.bind(topicManQueue()).to(topicExchange()).with(MAN).noargs();
  }

   // topic_queue队列绑定topic_exchange交换机,绑定的键为topic.man
   // 这样只要是消息携带的路由键是topic.man,才会分发到该队列
   @Bean
   public Binding bindingExchangeMessage2(){
       return BindingBuilder.bind(topicQueue()).to(topicExchange()).with(PERSON).noargs();
  }

}

 

2.发送消息
/**
    * topic模式发送消息到routingkey为topic.man的交换机中
    *
    * @author Wcj
    * @date: 2021/7/15 16:38
    */
   @GetMapping("/topicMessageMan")
   public String topicMessageMan() {
       String messageId = String.valueOf(UUID.randomUUID());
       String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
       Map<String, String> map = new HashMap<>();
       map.put("messageId", messageId);
       map.put("createTime", createTime);
       String message = "topic类型的交换机模式消息";
       map.put("messageData", message);
       // 发送消息给routing为man的队列
       rabbitTemplate.convertAndSend(TopicRabbitConfig.TOPIC_EXCHANGE, TopicRabbitConfig.MAN, map);
       return "success";
  }

   /**
    * topic模式发送消息到routingkey为topic.开头的交换机中
    *
    * @author Wcj
    * @date: 2021/7/15 16:38
    */
   @GetMapping("/topicMessagePerson")
   public String topicMessagePerson() {
       String messageId = String.valueOf(UUID.randomUUID());
       String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
       Map<String, String> map = new HashMap<>();
       map.put("messageId", messageId);
       map.put("createTime", createTime);
       String message = "topic类型的交换机模式消息";
       map.put("messageData", message);
       // 发送消息给routing为topic开头的队列
       rabbitTemplate.convertAndSend(TopicRabbitConfig.TOPIC_EXCHANGE, "topic.zhangsan", map);
       return "success";
  }

 

消费者

创建Rabbitmq监听类

/**
* @author Wcj
* @description: 监听topic消息的监听类
* @date 2021/7/15 15:31
*/
@Component
@RabbitListener(queues = "topic_queue_man")
public class TopicReceviverOne {

   @RabbitHandler
   public void process(Map topicMessage){
       System.out.println("topic消费者监听到队列为topic_queue_man,routingkey为topic.man的消息:"+topicMessage.toString());
  }
}

只能监听到topic.man开头的消息

 

/**
* @author Wcj
* @description: 监听topic消息的监听类
* @date 2021/7/15 15:31
*/
@Component
@RabbitListener(queues = "topic_queue")
public class TopicReceviverTwo {

   @RabbitHandler
   public void process(Map topicMessage){
       System.out.println("topic消费者监听到队列为topic_queue,routingkey为topic开头的消息:"+topicMessage.toString());
  }
}

两条topic开头的消息都能监听到

 

 

四丶Fanout(扇形交换机)交换机模型

无RoutingKey绑定

生产者

1.配置类
/**
* @author Wcj
* @description: 扇型交换机的配置类
* @date 2021/7/15 16:23
*/
@Configuration
public class FanoutRabbitConfig {

   public static final String FANOUT_QUEUE_ONE = "fanout_queue_one";

   public static final String FANOUT_QUEUE_TWO = "fanout_queue_two";

   public static final String FANOUT_QUEUE_THREE = "fanout_queue_three";

   public static final String FANOUT_EXCHANGE = "fanout_exchange";

   /**
    * 创建三个队列 :fanout.A   fanout.B fanout.C
    * 将三个队列都绑定在交换机 fanoutExchange 上
    * 因为是扇型交换机, 路由键无需配置,配置也不起作用
    */

   @Bean
   public Queue fanoutQueueOne(){
       return new Queue(FANOUT_QUEUE_ONE);
  }

   @Bean
   public Queue fanoutQueueTwo(){
       return new Queue(FANOUT_QUEUE_TWO);
  }

   @Bean
   public Queue fanoutQueueThree(){
       return new Queue(FANOUT_QUEUE_THREE);
  }

   @Bean
   public Exchange fanoutExchange(){
       return new FanoutExchange(FANOUT_EXCHANGE);
  }

   @Bean
   public Binding fanoutExchangeQueueOne(){
       return BindingBuilder.bind(fanoutQueueOne()).to(fanoutExchange()).with("").noargs();
  }

   @Bean
   public Binding fanoutExchangeQueueTwo(){
       return BindingBuilder.bind(fanoutQueueTwo()).to(fanoutExchange()).with("").noargs();
  }

   @Bean
   public Binding fanoutExchangeQueueThree(){
       return BindingBuilder.bind(fanoutQueueThree()).to(fanoutExchange()).with("").noargs();
  }

}

 

2.发送消息
 /**
    * fanout发送消息到fanout开头的交换机中
    * fanout模式不需要指定rontingkey,指定也不会生效
    *
    * @author Wcj
    * @date: 2021/7/15 16:38
    */
   @GetMapping("/fanoutMessage")
   public String fanoutMessage() {
       String messageId = String.valueOf(UUID.randomUUID());
       String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
       Map<String, String> map = new HashMap<>();
       map.put("messageId", messageId);
       map.put("createTime", createTime);
       String message = "fanout类型的交换机模式消息";
       map.put("messageData", message);
       // 发送消息给routing为topic开头的队列
       rabbitTemplate.convertAndSend(FanoutRabbitConfig.FANOUT_EXCHANGE, "", map);
       return "success";
  }

交换机会给三个队列都发送消息,不需要匹配routingKey

 

消费者

这里有三个消费者,只演示一个

/**
* @author Wcj
* @description: 监听fanout模式队列为fanout_queue_one消息的监听类
* @date 2021/7/15 14:35
*/
@Component
@RabbitListener(queues = "fanout_queue_one")
public class FanoutReceiverOne {


   @RabbitHandler
   public void process(Map message){
       System.out.println("FanoutReceiverOne消费者收到消息:"+message.toString());
  }
}

三个消费者分开监听不同的队列,分别接收到同一个交换机发出的消息

 

 

五丶生产者消息回调确认机制

1.在生产者的配置文件中加入配置

#确认消息已发送到交换机

publisher-confirm-type: correlated

#确认消息已发送到队列(Queue)

publisher-returns: true

server:
port: 10096
spring:
application:
  name: mq-rabbitmq-producer
 # 配置rabbirmq服务器
rabbitmq:
  host: localhost
  port: 5672
  username: root
  password: root
   # 虚拟host,可以不设置,使用server默认host
  virtualHost: /zzzwww
   # 确认消息已发送到交换机
  publisher-confirm-type: correlated
   #确认消息已发送到队列(Queue)
  publisher-returns: true
  template:
    retry:
      enabled: true
      initial-interval: 10000ms
      max-interval: 300000ms
      multiplier: 2

 

2.配置相关的回调函数

/**
* @author Wcj
* @description: rabbitmq配置文件
* @date 2021/7/15 17:04
*/
@Configuration
public class RabbitConfig {

   @Bean
   public RabbitTemplate createRabbitTemplate(ConnectionFactory connectionFactory){
       RabbitTemplate rabbitTemplate = new RabbitTemplate();
       rabbitTemplate.setConnectionFactory(connectionFactory);
       //设置开启Mandatory,才能触发回调函数,无论消息推送结果怎么样都强制调用回调函数
       rabbitTemplate.setMandatory(true);

       rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
           @Override
           public void confirm(CorrelationData correlationData, boolean ack, String cause) {
               System.out.println("ConfirmCallback:     "+"相关数据:"+correlationData);
               System.out.println("ConfirmCallback:     "+"确认情况:"+ack);
               System.out.println("ConfirmCallback:     "+"原因:"+cause);
          }
      });

       rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
           @Override
           public void returnedMessage(ReturnedMessage returnedMessage) {
               System.out.println("ReturnCallback:     "+"消息:"+returnedMessage.getMessage());
               System.out.println("ReturnCallback:     "+"回应码:"+returnedMessage.getReplyCode());
               System.out.println("ReturnCallback:     "+"回应信息:"+returnedMessage.getReplyText());
               System.out.println("ReturnCallback:     "+"交换机:"+returnedMessage.getExchange());
               System.out.println("ReturnCallback:     "+"路由键:"+returnedMessage.getRoutingKey());
          }
      });
       return rabbitTemplate;
  }
}

我们在上面写了两个回调函数一个叫:ConfirmCallback ,一个叫RetrunCallback

测试什么情况下会触发上面两种回调函数

  1. 消息推送到server,但是在server里找不到交换机

  2. 消息推送到server,找到交换机了,但是没找到队列

  3. 消息推送到sever,交换机和队列啥都没找到

  4. 消息推送成功

 

①丶测试交换机不存在的情况

编写测试接口,将消息发送到不存在的交换机中(non-existent-exchange)

/**
    * 生产者自动回调测试
    * 测试不存在的交换机和队列自动调用回调函数
    *
    * @author Wcj
    * @date: 2021/7/15 17:11
    */
   @GetMapping("/noExistMessageAck")
   public String TestMessageAck() {
       String messageId = String.valueOf(UUID.randomUUID());
       String messageData = "message: non-existent-exchange test message ";
       String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
       Map<String, Object> map = new HashMap<>();
       map.put("messageId", messageId);
       map.put("messageData", messageData);
       map.put("createTime", createTime);
       rabbitTemplate.convertAndSend("non-existent-exchange", "TestDirectRouting", map);
       return "ok";
  }

结论:调用接口会回调用ConfirmCallback函数

RabbitMQ学习及SpringBoot集成RabbitMQ_spring_13

 

②丶消息推送到交换机但是没找到队列情况

只声明交换机,不绑定任何队列

/**
* @author Wcj
* @description: 测试交换机存在,队列不存在的生产者异常情况
* @date 2021/7/15 17:22
*/
@Configuration
public class TestNoExistExchangeConfig {

   public static final String EXIST_EXCHANGE = "exist_exchange";

   // 只声明了交换机,没有绑定队列
   @Bean
   public Exchange existExchange(){
       return new DirectExchange(EXIST_EXCHANGE);
  }
}

 

/**
    * 生产者自动回调测试
    * 测试不存在的队列自动调用回调函数
    *
    * @author Wcj
    * @date: 2021/7/15 17:11
    */
   @GetMapping("/noExistQueueMessageAck")
   public String noExistQueueMessageAck() {
       String messageId = String.valueOf(UUID.randomUUID());
       String messageData = "message: non-existent-exchange test message ";
       String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
       Map<String, Object> map = new HashMap<>();
       map.put("messageId", messageId);
       map.put("messageData", messageData);
       map.put("createTime", createTime);
       rabbitTemplate.convertAndSend(TestNoExistExchangeConfig.EXIST_EXCHANGE, "TestDirectRouting", map);
       return "ok";
  }

结论:这种情况触发的是 ConfirmCallback和RetrunCallback两个回调函数

RabbitMQ学习及SpringBoot集成RabbitMQ_spring_14

 

 

③丶消息推送到sever,交换机和队列都找不到

结论:这种情况和第一种情况是一样的,会直接调用ConfirmCallback函数

 

④丶消息发送成功

结论:会调用ConfirmCallback函数

RabbitMQ学习及SpringBoot集成RabbitMQ_ide_15

 

 

六丶消费者消息回调确认机制

消费者消息确认机制和生产者确认机制不同,因为消费者在监听消息的同时也是在确认消息,所以消费者确认机制分为三种

1.自动确认:

这也是默认的消息确认情况。 AcknowledgeMode.NONERabbitMQ成功将消息发出(即将消息成功写入TCP Socket)中立即认为本次投递已经被正确处理,不管消费者端是否成功处理本次投递。所以这种情况如果消费端消费逻辑抛出异常,也就是消费端没有处理成功这条消息,那么就相当于丢失了消息。一般这种情况我们都是使用try catch捕捉异常后,打印日志用于追踪数据,这样找出对应数据再做后续处理。

 

2.手动确认

我们在配置接受消息确认机制时,常采用这种模式

  • basic.ack用于肯定确认

  • basic.nack用于否定确认(注意:这是AMQP 0-9-1的RabbitMQ扩展)

  • basic.reject用于否定确认,但与basic.nack相比有一个限制:一次只能拒绝单条消息

消费者端以上的3个方法都表示消息已经被正确投递,但是basic.ack表示消息已经被正确处理。 而basic.nack,basic.reject表示没有被正确处理

 

  • 消息重新入列场景需要用到Reject

channel.basicReject(deliveryTag, true); 拒绝消费当前消息,如果第二参数传入true,就是将数据重新丢回队列里,那么下次还会消费这消息。设置false,就是告诉服务器,我已经知道这条消息数据了,因为一些原因拒绝它,而且服务器也把这个消息丢掉就行。 下次不想再消费这条消息了。

使用拒绝后重新入列这个确认模式要谨慎,因为一般都是出现异常的时候,catch异常再拒绝入列,选择是否重入列。

但是如果使用不当会导致一些每次都被你重入列的消息一直消费-入列-消费-入列这样循环,会导致消息积压。

 

  • 设置不消费某条消息的场景需要用到nack

channel.basicNack(deliveryTag, false, true); 第一个参数依然是当前消息到的数据的唯一id; 第二个参数是指是否针对多条消息;如果是true,也就是说一次性针对当前通道的消息的tagID小于当前这条消息的,都拒绝确认。 第三个参数是指是否重新入列,也就是指不确认的消息是否重新丢回到队列里面去。

同样使用不确认后重新入列这个确认模式要谨慎,因为这里也可能因为考虑不周出现消息一直被重新丢回去的情况,导致积压。

 

代码:

1. 在消费者创建一个手动确认消息监听类
/**
* @author Wcj
* @description:
* @date 2021/7/16 15:05
*/
@Component
public class MyAckReceiver implements ChannelAwareMessageListener {
   @Override
   public void onMessage(Message message, Channel channel) throws Exception {
       long deliveryTag = message.getMessageProperties().getDeliveryTag();
       try {
           // 因为传递消息的时候用的map传递,所以将Map从Message内取出需要做些处理
           String msg = message.toString();
           String[] msgArray = msg.split("'");//可以点进Message里面看源码,单引号直接的数据就是我们的map消息数据
           Map<String, String> msgMap = mapStringToMap(msgArray[1].trim(),3);
           String messageId=msgMap.get("messageId");
           String messageData=msgMap.get("messageData");
           String createTime=msgMap.get("createTime");
           if ("direct_queue".equals(message.getMessageProperties().getConsumerQueue())){
               System.out.println("消费的消息来自的队列名为:"+message.getMessageProperties().getConsumerQueue());
               System.out.println("消息成功消费到 messageId:"+messageId+" messageData:"+messageData+" createTime:"+createTime);
               System.out.println("执行TestDirectQueue中的消息的业务处理流程......");

          }

           if ("fanout_queue_one".equals(message.getMessageProperties().getConsumerQueue())){
               System.out.println("消费的消息来自的队列名为:"+message.getMessageProperties().getConsumerQueue());
               System.out.println("消息成功消费到 messageId:"+messageId+" messageData:"+messageData+" createTime:"+createTime);
               System.out.println("执行fanout.A中的消息的业务处理流程......");

          }

           // 第二个参数,手动确认可以被批处理,当该参数为 true 时,则可以一次性确认 delivery_tag 小于等于传入值的所有消息
           channel.basicAck(deliveryTag, true);
           // channel.basicReject(deliveryTag, true);
           // 第二个参数,true会重新放回队列,所以需要自己根据业务逻辑判断什么时候使用拒绝
      } catch (Exception e) {
           channel.basicReject(deliveryTag, false);
           e.printStackTrace();
      }

  }

   //{key=value,key=value,key=value} 格式转换成map
   private Map<String, String> mapStringToMap(String str,int entryNum ) {
       str = str.substring(1, str.length() - 1);
       String[] strs = str.split(",",entryNum);
       Map<String, String> map = new HashMap<String, String