RabbitMQ学习笔记(三) 之SpringBoot集成

92 阅读18分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第8天,点击查看活动详情

1、集成所需前置

1. pom.xml 添加 RabbitMQ 依赖包

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

2. 添加yml相关配置

# 数据源配置
spring:
    # rabbitmq 配置
    rabbitmq:
        host: 127.0.0.1
        port: 5672
        username: admin
        password: admin
        #开启消息确认模式,新版本已经弃用
        #publisher-confirms: true
        #开启消息送达提示
        publisher-returns: true
        # springboot.rabbitmq.publisher-confirm 新版本已被弃用,现在使用 spring.rabbitmq.publisher-confirm-type = correlated 实现相同效果
        publisher-confirm-type: correlated
        #虚拟host 可以不设置,我自己的是test
        virtual-host: /test
        listener:
            type: simple
            simple:
                acknowledge-mode: auto #确认模式
                prefetch: 1 #限制每次发送一条数据。
                concurrency: 1 #同一个队列启动几个消费者
                max-concurrency: 3 #启动消费者最大数量
                #重试策略相关配置
                retry:
                    # 开启消费者(程序出现异常)重试机制,默认开启并一直重试
                    enabled: true
                    # 最大重试次数
                    max-attempts: 5
                    # 重试间隔时间(毫秒)
                    initial-interval: 3000

ps:里面的虚拟host配置项不是必须的,我自己在rabbitmq服务上创建了自己的虚拟host,所以我配置了;不进行创建,就不用加这个配置项,直接用 / 就行。

那么怎么建一个单独的host呢? 假如我就是想给某个项目接入,使用一个单独host,顺便使用一个单独的账号,就好像我文中配置的 root 这样。

其实也很简单,virtual-host的创建:

image.png 因为我用admin账号登录创建的,所以默认就给这个虚拟主机配置了当前账号和全部权限,想给别的用户添加权限的话,点击name进去设置就行了。

2、集成与测试

RabbitMQ常量,存放交换器名称和队列名称以及路由键:

public class RabbitMqConst {

    // 交换器
    public static final  String EXCHANGE_DIRECT = "test-direct-exchange";
    public static final  String EXCHANGE_TOPIC = "test-topic-exchange";
    public static final  String EXCHANGE_FANOUT = "test-fanout-exchange";

    // 队列
    public static final  String QUEQE_DIRECT_1 = "test-direct-queue-1";
    public static final  String QUEQE_DIRECT_2 = "test-direct-queue-2";
    public static final  String QUEQE_FANOUT_1 = "test-fanout-queue-1";
    public static final  String QUEQE_FANOUT_2 = "test-fanout-queue-2";
    public static final  String QUEQE_FANOUT_3 = "test-fanout-queue-3";

    public static final  String QUEQE_TOPIC_A = "test-topic-queue-a";
    public static final  String QUEQE_TOPIC_B = "test-topic-queue-b";


    // 路由键
    public static final  String sayDirect = "say-direct";
    public static final  String sayDirect2 = "say-direct-2";

    public static final  String sayTopicA = "say-topic.a";
    public static final  String sayTopicB = "say-topic.b";

}

RabbitMQ配置类,进行交换器,队列和路由键绑定,核心代码:


import org.springframework.amqp.core.AcknowledgeMode;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 类说明:RabbitMQ 相关配置,核心代码
 */
@Configuration
public class RabbitMQConfig {


  //===============验证Direct Exchange的交换器 ==========
  //TODO 申明队列
  @Bean
  public Queue directQueue() {
    return new Queue(RabbitMqConst.QUEQE_DIRECT_1);
  }
  @Bean
  public Queue directQueue2() {
    return new Queue(RabbitMqConst.QUEQE_DIRECT_2);
  }
  //TODO 申明Direct交换机
  @Bean
  DirectExchange directExchange() {
    return new DirectExchange(RabbitMqConst.EXCHANGE_DIRECT,true,false);
  }
  @Bean
  DirectExchange directExchange2() {
    return new DirectExchange(RabbitMqConst.EXCHANGE_DIRECT,true,false);
  }

  //TODO 将队列和交换机绑定, 并设置路由键
  @Bean
  Binding bindingDirect() {
    return BindingBuilder.bind(directQueue()).to(directExchange()).with(RabbitMqConst.sayDirect);
  }
  @Bean
  Binding bindingDirect2() {
    return BindingBuilder.bind(directQueue2()).to(directExchange2()).with(RabbitMqConst.sayDirect2);
  }
  //===============以上是验证Direct Exchange的交换器==========



  //===============以下是验证Fanout Exchange================
  /**
   *  TODO 申明广播队列
   *  创建三个队列 :QUEQE_FANOUT_1,QUEQE_FANOUT_2,QUEQE_FANOUT_3
   *  将三个队列都绑定在交换机 fanoutExchange 上
   *  因为是扇型交换机, 路由键无需配置,配置也不起作用
   */
  @Bean
  public Queue fanoutQueue1() {
    return new Queue(RabbitMqConst.QUEQE_FANOUT_1);
  }
  @Bean
  public Queue fanoutQueue2() {
    return new Queue(RabbitMqConst.QUEQE_FANOUT_2);
  }
  @Bean
  public Queue fanoutQueue3() {
    return new Queue(RabbitMqConst.QUEQE_FANOUT_3);
  }

  //TODO 申明Fanout交换器
  @Bean
  public FanoutExchange fanoutExchange() {
    return new FanoutExchange(RabbitMqConst.EXCHANGE_FANOUT);
  }

  //TODO 绑定关系
  @Bean
  Binding bindingExchange1(Queue fanoutQueue1, FanoutExchange fanoutExchange) {
    return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
  }
  @Bean
  Binding bindingExchange2(Queue fanoutQueue2, FanoutExchange fanoutExchange) {
    return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
  }
  @Bean
  Binding bindingExchange3(Queue fanoutQueue3, FanoutExchange fanoutExchange) {
    return BindingBuilder.bind(fanoutQueue3).to(fanoutExchange);
  }
  //===============以上是验证Fanout Exchange的交换器==========




  //===============以下是验证Topic Exchange================
  //TODO 申明队列
  @Bean
  public Queue topicQueueA() {
    return new Queue(RabbitMqConst.QUEQE_TOPIC_A);
  }
  @Bean
  public Queue topicQueueB() {
    return new Queue(RabbitMqConst.QUEQE_TOPIC_B);
  }
  //TODO 申明Topic交换器
  @Bean
  TopicExchange topicExchange() {
    return new TopicExchange(RabbitMqConst.EXCHANGE_TOPIC);
  }

  //TODO 将队列和交换机绑定, 并设置路由键
  @Bean
  Binding bindingExchangeMessageA() {
    return BindingBuilder.bind(topicQueueA()).to(topicExchange()).with("say-topic.#");
  }
  @Bean
  Binding bindingExchangeMessageB() {
    return BindingBuilder.bind(topicQueueB()).to(topicExchange()).with("#.b");
  }
  //===============以上是验证Topic Exchange的交换器==========


  //===============以下是生产者推送消息的消息确认调用回调函数==========
  @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.setReturnCallback(new RabbitTemplate.ReturnCallback() {
      @Override
      public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        System.out.println("ReturnCallback:     "+"消息:"+message);
        System.out.println("ReturnCallback:     "+"回应码:"+replyCode);
        System.out.println("ReturnCallback:     "+"回应信息:"+replyText);
        System.out.println("ReturnCallback:     "+"交换机:"+exchange);
        System.out.println("ReturnCallback:     "+"路由键:"+routingKey);
      }
    });

    return rabbitTemplate;
  }
  //===============以上是生产者推送消息的消息确认调用回调函数==========


  //===============以下是新增一个直连交换机,但不给它做任何绑定配置操作==========
  @Bean
  DirectExchange lonelyDirectExchange() {
    return new DirectExchange("lonelyDirectExchange");
  }
  //===============以上是新增一个直连交换机,但不给它做任何绑定配置操作==========



  //===============以下是一般的消息接收手动确认========================
  @Autowired
  private CachingConnectionFactory connectionFactory;
  @Autowired
  private MyAckReceiver myAckReceiver;//消息接收处理类

  @Bean
  public SimpleMessageListenerContainer simpleMessageListenerContainer() {
    SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory);
    container.setConcurrentConsumers(1);
    container.setMaxConcurrentConsumers(1);
    // RabbitMQ默认是自动确认,这里改为手动确认消息
    container.setAcknowledgeMode(AcknowledgeMode.MANUAL);
    //设置一个队列
    container.setQueueNames(RabbitMqConst.QUEQE_DIRECT_1,RabbitMqConst.QUEQE_DIRECT_2);
    //如果同时设置多个如下: 前提是队列都是必须已经创建存在的
    //  container.setQueueNames("TestDirectQueue","TestDirectQueue2","TestDirectQueue3");


    //另一种设置队列的方法,如果使用这种情况,那么要设置多个,就使用addQueues
    //container.setQueues(new Queue("TestDirectQueue",true));
    //container.addQueues(new Queue("TestDirectQueue2",true));
    //container.addQueues(new Queue("TestDirectQueue3",true));
    container.setMessageListener(myAckReceiver);

    return container;
  }

  //===============以上是一般的消息接收手动确认========================


// -------------------以下是定义延时队列 ------------------------------
@Bean("delayQueue")
public Queue delayQueue(){
  //设置死信交换机和路由key
  return QueueBuilder.durable("delayQueue")
      //如果消息过时,则会被投递到当前对应的my-dlx-exchange
      .withArgument("x-dead-letter-exchange","my-dlx-exchange")
      //如果消息过时,my-dlx-exchange会更具routing-key-delay投递消息到对应的队列
      .withArgument("x-dead-letter-routing-key","routing-key-delay").build();
}
//定义死信队列
@Bean("dlxQueue")
public Queue dlxQueue(){
  return QueueBuilder.durable("my-dlx-queue").build();
}
//定义死信交换机
@Bean("dlxExchange")
public Exchange dlxExchange(){
  return ExchangeBuilder.directExchange("my-dlx-exchange").build();
}
//绑定死信队列与交换机
@Bean("dlxBinding")
public Binding dlxBinding(@Qualifier("dlxExchange") Exchange exchange,@Qualifier("dlxQueue") Queue queue){
  return BindingBuilder.bind(queue).to(exchange).with("routing-key-delay").noargs();
}
// -------------------以上是定义延时队列 ------------------------------

}

1. dircet交换器消息生产与消费

controller类:用于调用direct测试方法: image.png

定义两个消费者监听消息: image.png

运行结果:

18:03:05.948 [http-nio-8080-exec-2] INFO  c.r.w.c.k.r.RabbitMqController - [sendDirectMessage,43] - ------> 发送的消息为:{createTime=2022-11-27 18:03:05, messageId=25a0ac83-c29f-4e3a-abd7-c008c7a876df, messageData=111}
-----> B接收到来自test-direct-queue的消息:[{createTime=2022-11-27 18:03:05, messageId=25a0ac83-c29f-4e3a-abd7-c008c7a876df, messageData=111}]
18:03:08.459 [http-nio-8080-exec-3] INFO  c.r.w.c.k.r.RabbitMqController - [sendDirectMessage,43] - ------> 发送的消息为:{createTime=2022-11-27 18:03:08, messageId=bde4ede7-5f39-4116-99a3-6615fe65fde0, messageData=222}
-----> A接收到来自test-direct-queue的消息:[{createTime=2022-11-27 18:03:08, messageId=bde4ede7-5f39-4116-99a3-6615fe65fde0, messageData=222}]

可以看到是实现了轮询的方式对消息进行消费,而且不存在重复消费。

2. fount交换器消息生产与消费

controller类:用于调用fount测试方法:

image.png

定义三个消费者监听消息:

image.png

运行结果:

18:35:23.615 [http-nio-8080-exec-3] INFO  c.r.w.c.k.r.RabbitMqController - [sendFountMessage,54] - ------> 发送的消息为:{createTime=2022-11-27 18:35:23, messageId=9ab0b75f-619b-4a00-b209-cbd16635f0e9, messageData=我是fount}
-----> 3接收到来自test-fanout-queue-3的消息:[{createTime=2022-11-27 18:35:23, messageId=9ab0b75f-619b-4a00-b209-cbd16635f0e9, messageData=我是fount}]
-----> 1接收到来自test-fanout-queue-1的消息:[{createTime=2022-11-27 18:35:23, messageId=9ab0b75f-619b-4a00-b209-cbd16635f0e9, messageData=我是fount}]
-----> 2接收到来自test-fanout-queue-2的消息:[{createTime=2022-11-27 18:35:23, messageId=9ab0b75f-619b-4a00-b209-cbd16635f0e9, messageData=我是fount}]

3. topic交换器消息生产与消费

controller类:用于调用Topic测试方法: image.png

定义两个个消费者监听消息:

image.png

方法A运行结果:

19:38:12.644 [http-nio-8080-exec-16] INFO  c.r.w.c.k.r.RabbitMqController - [sendTopicMessageA,69] - ------> 发送的消息为:{createTime=2022-11-27 19:38:12, messageId=c6004009-69e2-46c4-a148-297fcbf91b0c, messageData=我从send_topic_msg_a来}
-----> A接收到来自test-topic-queue-a的消息:[{createTime=2022-11-27 19:38:12, messageId=c6004009-69e2-46c4-a148-297fcbf91b0c, messageData=我从send_topic_msg_a来}]

执行方法A的时候: 队列A,绑定键为:say-topic.# 队列B,绑定键为:#.b 而当前推送的消息,携带的路由键为:say-topic.a

所以可以看到监听消费者A成功消费到了消息,因为只有监听的队列A所绑定键能与这条消息携带的路由键匹配上。

方法B运行结果:

19:38:23.984 [http-nio-8080-exec-11] INFO  c.r.w.c.k.r.RabbitMqController - [sendTopicMessageB,81] - ------> 发送的消息为:{createTime=2022-11-27 19:38:23, messageId=d4023d86-b341-47a2-b802-a6c1be2df8b2, messageData=我从send_topic_msg_b来}
-----> B接收到来自test-topic-queue-b的消息:[{createTime=2022-11-27 19:38:23, messageId=d4023d86-b341-47a2-b802-a6c1be2df8b2, messageData=我从send_topic_msg_b来}]
-----> A接收到来自test-topic-queue-a的消息:[{createTime=2022-11-27 19:38:23, messageId=d4023d86-b341-47a2-b802-a6c1be2df8b2, messageData=我从send_topic_msg_b来}]

执行方法B的时候: 队列A,绑定键为:say-topic.# 队列B,绑定键为:#.b 而当前推送的消息,携带的路由键为:say-topic.b

所以可以看到两个监听消费者都成功消费到了消息,因为这两个监听的队列的绑定键都能与这条消息携带的路由键匹配上。

4. 生产者推送消息的消息确认回调函数使用

生产者推送消息的消息确认调用回调函数相关配置再 RabbitMQConfig

可以看到里面写了两个回调函数,一个叫 ConfirmCallback ,一个叫 RetrunCallback; 那么以上这两种回调函数都是在什么情况会触发呢?

先从总体的情况分析,推送消息存在四种情况:

  1. 消息推送到mq,但是在mq里找不到交换机
  2. 消息推送到mq,找到交换机了,但是没找到队列
  3. 消息推送到mq,交换机和队列啥都没找到
  4. 消息推送成功
第一种:消息推送到mq,但是在mq里找不到交换机

image.png

运行结果:

19:49:59.033 [http-nio-8080-exec-1] INFO  c.r.w.c.k.r.RabbitMqController - [sendNoExchangeMessage,98] - ------> 发送的消息为:{createTime=2022-11-27 19:49:59, messageId=2b93dff3-d2b4-4f14-aed8-49b81425d2ec, messageData=aa}
19:49:59.046 [AMQP Connection 127.0.0.1:5672] ERROR o.s.a.r.c.CachingConnectionFactory - [log,748] - Shutdown Signal: channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'no-exchange' in vhost '/test', class-id=60, method-id=40)
ConfirmCallback:     相关数据:null
ConfirmCallback:     确认情况:false
ConfirmCallback:     原因:channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'no-exchange' in vhost '/test', class-id=60, method-id=40)

这种情况触发的是 ConfirmCallback 回调函数。

第二种:消息推送到mq,找到交换机了,但是没找到队列

image.png

20:12:51.052 [http-nio-8080-exec-3] INFO  c.r.w.c.k.r.RabbitMqController - [sendNoQueueMessage,113] - ------> 发送的消息为:{createTime=2022-11-27 20:12:51, messageId=26db55de-142d-4aa4-981c-774fcc994902, messageData=aa}
ReturnCallback:     消息:(Body:'[serialized object]' MessageProperties [headers={}, contentType=application/x-java-serialized-object, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, deliveryTag=0])
ReturnCallback:     回应码:312
ReturnCallback:     回应信息:NO_ROUTE
ReturnCallback:     交换机:lonelyDirectExchange
ReturnCallback:     路由键:say-direct
ConfirmCallback:     相关数据:null
ConfirmCallback:     确认情况:true
ConfirmCallback:     原因:null

可以看到这种情况,两个函数都被调用了; 这种情况下,消息是推送成功到服务器了的,所以ConfirmCallback对消息确认情况是true; 而在RetrunCallback回调函数的打印参数里面可以看到,消息是推送到了交换机成功了,但是在路由分发给队列的时候,找不到队列,所以报了错误 NO_ROUTE 。

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

第三种:消息推送到mq,交换机和队列啥都没找到

这种情况其实一看就觉得跟第一种很像,第一种和第三种情况回调是一致的。

结论: 这种情况触发的是 ConfirmCallback 回调函数。

第四种:消息推送成功

直接调用之前direct的示例,运行结果如下:

20:18:38.544 [http-nio-8080-exec-2] INFO  c.r.w.c.k.r.RabbitMqController - [sendDirectMessage,39] - ------> 发送的消息为:{createTime=2022-11-27 20:18:38, messageId=999afdff-e27d-477c-9070-19d517c72147, messageData=aa}
ConfirmCallback:     相关数据:null
ConfirmCallback:     确认情况:true
ConfirmCallback:     原因:null
-----> A接收到来自test-direct-queue的消息:[{createTime=2022-11-27 20:18:38, messageId=999afdff-e27d-477c-9070-19d517c72147, messageData=aa}]

5. 消费者接收到消息的消息确认机制

和生产者的消息确认机制不同,因为消息接收本来就是在监听消息,符合条件的消息就会消费下来。 所以,消息接收的确认机制主要存在三种模式:

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

  2. 根据情况确认, 这个不做介绍

  3. 手动确认 , 这个比较关键,也是我们配置接收消息确认机制时,多数选择的模式。 消费者收到消息后,手动调用 basic.ack / basic.nack / basic.reject 后,RabbitMQ收到这些消息后,才认为本次投递成功。

  • 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小于当前这条消息的,都拒绝确认。

第三个参数是指是否重新入列,也就是指不确认的消息是否重新丢回到队列里面去。

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

之前的 CustomerListener 中关于 Direct消息相关监听器可以先注释掉,以免造成多个同类型监听器都监听同一个队列。

新建消息接收处理类:MyAckReceiver

@Component
public class MyAckReceiver implements ChannelAwareMessageListener {

  @Override
  public void onMessage(Message message, Channel channel) throws Exception {
    long deliveryTag = message.getMessageProperties().getDeliveryTag();
    try {
      byte[] body = message.getBody();
      ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(body));
      Map<String,String> msgMap = (Map<String,String>) ois.readObject();
      String messageId = msgMap.get("messageId");
      String messageData = msgMap.get("messageData");
      String createTime = msgMap.get("createTime");
      ois.close();
      System.out.println("  MyAckReceiver  messageId:"+messageId+"  messageData:"+messageData+"  createTime:"+createTime);
      System.out.println("消费的主题消息来自:"+message.getMessageProperties().getConsumerQueue());
      //第二个参数,手动确认可以被批处理,当该参数为 true 时,则可以一次性确认 delivery_tag 小于等于传入值的所有消息
      channel.basicAck(deliveryTag, true);
      //第二个参数,true会重新放回队列,所以需要自己根据业务逻辑判断什么时候使用拒绝
//      channel.basicReject(deliveryTag, true);
    } catch (Exception e) {
      channel.basicReject(deliveryTag, false);
      e.printStackTrace();
    }
  }
}

Controller测试方法同 Direct测试方法,运行结果:

20:55:26.559 [http-nio-8080-exec-2] INFO  c.r.w.c.k.r.RabbitMqController - [sendDirectMessageByConfirm,129] - ------> 发送的消息为:{createTime=2022-11-27 20:55:26, messageId=6d14b83c-4520-4362-9be1-bcefdbf19ea2, messageData=aa}
  MyAckReceiver  messageId:6d14b83c-4520-4362-9be1-bcefdbf19ea2  messageData:aa  createTime:2022-11-27 20:55:26
消费的主题消息来自:test-direct-queue
ConfirmCallback:     相关数据:null
ConfirmCallback:     确认情况:true
ConfirmCallback:     原因:null

监听的多个消息队列都变为手动确认,并处理各自的业务:

第一步,往SimpleMessageListenerContainer里添加多个队列:

image.png

第二步,我们的手动确认消息监听类,MyAckReceiver.java 就可以同时将上面设置到的队列的消息都消费下来。 但是我们需要做不同的业务逻辑处理,那么只需要  根据消息来自的队列名进行区分处理即可,如:

@Component
public class MyAckReceiver implements ChannelAwareMessageListener {

  @Override
  public void onMessage(Message message, Channel channel) throws Exception {
    long deliveryTag = message.getMessageProperties().getDeliveryTag();
    try {
      byte[] body = message.getBody();
      ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(body));
      Map<String,String> msgMap = (Map<String,String>) ois.readObject();
      String messageId = msgMap.get("messageId");
      String messageData = msgMap.get("messageData");
      String createTime = msgMap.get("createTime");
      ois.close();

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

      }

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

      }

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

}

Controller方法测试类:

image.png

运行结果:

image.png

3、应用异步解耦,需要注意点

场景:

image.png

用户下订单买商品,订单处理成功后,去扣减库存,在这个场景里,订单系统是生产者,库存系统是消费者。库存是必须扣减的,在业务上来说,有库存直接扣减即可,没库存或者低于某个阈值,可以扣减成功,不过要通知其他系统(如通知采购系统尽快采购,通知用户订单系统我们会尽快调货)。

RPC 实现

通过 RPC 的实现,可以看到 RPC 会造成耦合。一旦库存系统失败,订单系统也会跟着失败。我们希望库存系统本身的失败,不影响订单系统的继续执行,在业务流程上,进行订单系统和库存系统的解耦。

RabbitMQ 的实现

对于我们消息模式的实现,为保证库存必须有扣减,我们要考虑几个问题:

  1. 订单系统发给 Mq 服务器的扣减库存的消息必须要被 Mq 服务器接收到,意味着需要使用发送者确认。
  2. Mq 服务器在扣减库存的消息被库存服务正确处理前必须一直保存,那么需要消息进行持久化。
  3. 某个库存服务器出了问题,扣减库存的消息要能够被其他正常的库存服务处理,需要我们自行对消费进行确认,意味着不能使用消费者自动确认,而应该使用手动确认。

所以生产者订单系统这边需要:配置文件中队列和交换器进行持久化,消息发送时的持久化,发送者确认的相关配置和代码。 而消费者库存系统这边要进行手动确认

4、实现限时订单过期

1669559165436.jpg

Controller方法测试类:

image.png

消费者:

image.png 运行结果:

23:27:25.783 [http-nio-8080-exec-2] INFO  c.r.w.c.k.r.RabbitMqController - [addDealQueue,167] - ------>当前时间:1669562845783
ConfirmCallback:     相关数据:null
ConfirmCallback:     确认情况:true
ConfirmCallback:     原因:null
----->当前时间:1669562875834, 数据过期:[{orderId=123}]

参考资料: 享学课堂king老师 & csdn博主-小目标青年 & 军大君