Spring Boot消息队列RabbitMQ的使用

220 阅读11分钟

gitee链接

rabbitmqDemo,消息队列的使用

Spring Boot版本:2.3.4.RELEASE

参考链接

目录

  • 使用RabbitMq的准备工作
  • 消息队列的三个常用交换机
  • 商品订单超时提醒的实现
  • 消费过程出现异常如何处理

使用RabbitMq的准备工作

rabbitmq环境准备(Docker容器):

docker run --name myrabbit    -p 15672:15672 -p 5672:5672 -p 25672:25672 -p 61613:61613 -p 1883:1883 -itd --restart=always -v /etc/localtime:/etc/localtime -v /home/mycontainers/myrabbit/data:/data --net mynetwork -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin rabbitmq:management

访问 http://myserverhost:15672 可以看到rabbit可视化界面

Maven依赖:

<!--消息队列-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

配置文件 application.yml:

server:
  port: 8888
spring:
  rabbitmq:
    host: myserverhost
    port: 5672
    username: admin
    password: admin
    # 消息确认配置项
    # 确认消息已发送到交换机(Exchange)
    publisher-confirm-type: correlated
    # 确认消息已发送到队列(Queue)
    publisher-returns: true

消息队列的三个常用交换机

消息队列的大致流程为:生产者 -> 交换机 -> 消费者

消息由生产者发出,经过交换机被消费者接收,交换机的常用类型有三种:

  • 直连交换机
  • 主题交换机
  • 扇形交换机

首先创建一个RabbitConstant的常量类:

package com.cc.config;

public class RabbitConstant {
    /**
     * 直连交换机
     */
    public static final String DirectQueue = "DirectQueue";
    public static final String DirectExchange = "DirectExchange";
    public static final String DirectRouting = "DirectRouting";
    public static final String LonelyDirectExchange = "LonelyDirectExchange";
    
    /**
     * 主题交换机
     */
    // 绑定键
    public final static String man = "topic.man";
    public final static String woman = "topic.woman";
    public static final String TopicExchange = "TopicExchange";

    /**
     * 扇形交换机
     */
    public final static String FanoutA = "fanout.A";
    public final static String FanoutB = "fanout.B";
    public final static String FanoutC = "fanout.C";
    public final static String FanoutExchange = "FanoutExchange";
    
    /**
     * 订单消息
     */
    public final static String OrderQueue = "OrderQueue";
    public final static String OrderExchange = "OrderExchange";
    public final static String OrderRouting= "OrderRouting";

    /**
     * 订单延迟队列
     */
    public final static String OrderDelayQueue = "OrderDelayQueue";
    public final static String OrderDelayExchange = "OrderDelayExchange";
    public final static String OrderDelayRouting = "OrderDelayRouting";
}

直连交换机

直连交换机是一对一的形式,消息只会被一个消费者接收,如果有多个消费者监听直连交换机的同一个队列,消息会以轮询的方式被消费,并不会重复消费。

消息生产者的配置类:

package com.cc.config.provider;

import com.cc.config.RabbitConstant;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 直连交换机
 * @author cc
 * @date 2021-11-27 10:20
 */
@Configuration
public class DirectRabbitConfig {
    // 队列
    @Bean
    public Queue directQueue() {
        // durable:是否持久化,默认false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在;暂存队列:当前连接有效
        // exclusive:默认为false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
        // autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除

        // 一般设置队列持久化就可以
        return new Queue(RabbitConstant.DirectQueue, true);
    }

    // Direct交换机
    @Bean
    DirectExchange directExchange() {
        return new DirectExchange(RabbitConstant.DirectExchange, true, false);
    }

    // 绑定,将队列和交换机绑定,并设置用于匹配键:TestDirectRouting
    @Bean
    Binding bindingDirect() {
        return BindingBuilder.bind(directQueue()).to(directExchange()).with(RabbitConstant.DirectRouting);
    }

    // 测试消息确认模块
    @Bean
    DirectExchange lonelyDirectExchange() {
        return new DirectExchange(RabbitConstant.LonelyDirectExchange);
    }
}

消费者消费类:

package com.cc.config.consumer;

import com.cc.config.RabbitConstant;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.util.Map;

/**
 * 直连交换机消费者
 * @author cc
 * @date 2021-11-27 10:55
 */
@Component
@RabbitListener(queues = RabbitConstant.DirectQueue)
public class DirectReceiver {
    @RabbitHandler
    public void process(Map testMsg) {
        System.out.println("DirectReceiver消费者收到消息: " + testMsg.toString());
    }
}

实际应用中,消息的生产者和消费者是分开来的,在示例中为了方便就集成到了一个项目里面,即我生产消息,我消费消息。

写接口测试:

private final RabbitTemplate rabbitTemplate;

public TestController(RabbitTemplate rabbitTemplate) {
    this.rabbitTemplate = rabbitTemplate;
}

@GetMapping("/sendDirectMsg")
public String sendDirectMsg() {
    Map<String, Object> map = generateMap("test message hello!");
    // 将消息携带绑定值:TestDirectRouting发送到交换机TestDirectExchange
    rabbitTemplate.convertAndSend(RabbitConstant.DirectExchange, RabbitConstant.DirectRouting, map);
    return "ok";
}

测试效果是消息刚刚生产出来就被消费了。

这种消息属于一对一。

主题交换机

根据通配符来将消息发送给指定的队列

生产者配置类:

package com.cc.config.provider;

import com.cc.config.RabbitConstant;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 主题交换机
 * @author cc
 * @date 2021-11-27 10:20
 */
@Configuration
public class TopicRabbitConfig {

    @Bean
    public Queue firstQueue() {
        return new Queue(RabbitConstant.man);
    }

    @Bean
    public Queue secondQueue() {
        return new Queue(RabbitConstant.woman);
    }

    @Bean
    TopicExchange exchange() {
        return new TopicExchange(RabbitConstant.TopicExchange);
    }

    /**
     * 将firstQueue和TopicExchange绑定,而且绑定值为topic.man
     * 这样只要是消息携带的路由器是topic.man,才会分发到该队列
     */
    @Bean
    Binding bindingExchangeMessage() {
        return BindingBuilder.bind(firstQueue()).to(exchange()).with(RabbitConstant.man);
    }

    /**
     * 将secondQueu和topicExchange绑定,而且绑定的键值为通配路由键topic.#
     * 这样只要是消息携带的路由器是以topic.开头,就都会分发到该队列
     */
    @Bean
    Binding bindingExchangeMessage2() {
        return BindingBuilder.bind(secondQueue()).to(exchange()).with("topic.#");
    }
}

在主题交换机中,我们配置了两个队列,一个队列响应我们指定的topic.man,一个队列响应满足topic.#规则的队列,关于匹配规则,有两个特殊字符:*和**#**,*表示任意一个词,#表示任意数量的词,当路由键为一个#时表示所有。

消费者消费类1:

package com.cc.config.consumer;

import com.cc.config.RabbitConstant;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.util.Map;

/**
 * 主题交换机消费者
 * @author cc
 * @date 2021-11-27 10:29
 */
@Component
@RabbitListener(queues = RabbitConstant.man)
public class TopicManReceiver {
    @RabbitHandler
    public void process(Map testMsg) {
        System.out.println("TopicManReceiver消费者收到消息:" + testMsg.toString());
    }
}

消费者消费类2:

package com.cc.config.consumer;

import com.cc.config.RabbitConstant;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.util.Map;

/**
 * 主题交换机消费者
 * @author cc
 * @date 2021-11-27 10:29
 */
@Component
@RabbitListener(queues = RabbitConstant.woman)
public class TopicTotalReceiver {
    @RabbitHandler
    public void process(Map testMsg) {
        System.out.println("TopicTotalReceiver:" + testMsg.toString());
    }
}

写接口测试:

@GetMapping("/sendTopicMsg1")
public String sendTopicMsg1() {
    Map<String, Object> map = generateMap("message: M A N");
    rabbitTemplate.convertAndSend(RabbitConstant.TopicExchange, RabbitConstant.man, map);
    return "ok";
}

@GetMapping("/sendTopicMsg2")
public String sendTopicMsg2() {
    Map<String, Object> map = generateMap("message: woman is all");
    rabbitTemplate.convertAndSend(RabbitConstant.TopicExchange, RabbitConstant.woman, map);
    return "ok";
}

测试结果为:

  • 当我们发送路由键为topic.man的消息时,消费者1和2都消费到了,因为消费者2监听的主题是topic.#,topic.man符合该规则。
  • 当我们发送路由键位topic.woman的消息时,只有消费者2消费到了,原因不用多说明了吧。

扇形交换机

多个队列绑定了同一个扇形交换机,发送到这个扇形交换机上的消息都会被多个队列消费到,即以广播的形式发送。

生产者配置类:

package com.cc.config.provider;

import com.cc.config.RabbitConstant;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 扇形交换机
 * @author cc
 * @date 2021-11-27 10:48
 */
@Configuration
public class FanoutRabbitConfig {
    /**
     * 创建三个队列:fanout.A  fanout.B  fanout.C
     * 将三个队列都绑定在交换机 fanoutExchange 上
     * 因为是扇形交换机,路由键无需配置,配置也不起作用
     */
    @Bean
    public Queue queueA() {
        return new Queue(RabbitConstant.FanoutA);
    }

    @Bean
    public Queue queueB() {
        return new Queue(RabbitConstant.FanoutB);
    }

    @Bean
    public Queue queueC() {
        return new Queue(RabbitConstant.FanoutC);
    }

    @Bean
    FanoutExchange fanoutExchange() {
        return new FanoutExchange(RabbitConstant.FanoutExchange);
    }

    @Bean
    Binding bindingExchangeA() {
        return BindingBuilder.bind(queueA()).to(fanoutExchange());
    }

    @Bean
    Binding bindingExchangeB() {
        return BindingBuilder.bind(queueB()).to(fanoutExchange());
    }

    @Bean
    Binding bindingExchangeC() {
        return BindingBuilder.bind(queueC()).to(fanoutExchange());
    }
}

消费者消费类1:

package com.cc.config.consumer;

import com.cc.config.RabbitConstant;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.util.Map;

/**
 * 扇形交换机消费者A
 */
@Component
@RabbitListener(queues = RabbitConstant.FanoutA)
public class FanoutReceiverA {
    @RabbitHandler
    public void process(Map testMsg) {
        System.out.println("FanoutReceiverA消费者收到消息:" + testMsg.toString());
    }
}

消费者消费类2:

package com.cc.config.consumer;

import com.cc.config.RabbitConstant;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.util.Map;

/**
 * 扇形交换机消费者A
 */
@Component
@RabbitListener(queues = RabbitConstant.FanoutB)
public class FanoutReceiverB {
    @RabbitHandler
    public void process(Map testMsg) {
        System.out.println("FanoutReceiverB消费者收到消息:" + testMsg.toString());
    }
}

消费者消费类3:

package com.cc.config.consumer;

import com.cc.config.RabbitConstant;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.util.Map;

/**
 * 扇形交换机消费者C
 */
@Component
@RabbitListener(queues = RabbitConstant.FanoutC)
public class FanoutReceiverC {
    @RabbitHandler
    public void process(Map testMsg) {
        System.out.println("FanoutReceiverC消费者收到消息:" + testMsg.toString());
    }
}

写接口测试:

@GetMapping("/sendFanoutMsg")
public String sendFanoutMsg() {
    Map<String, Object> map = generateMap("message: testFanoutMsg");
    rabbitTemplate.convertAndSend(RabbitConstant.FanoutExchange, null, map);
    return "ok";
}

测试结果为所有的消费者都收到消息了,因为大家都注册了同一个交换机。

商品订单超时提醒的实现

rabbitmq里没有提供直接的超时提醒交换机,但是可以通过死信队列来变相实现,实现思路是这样的:

  1. 准备两个队列,订单延迟队列和订单队列
  2. 业务逻辑生成订单时,发送一条消息到订单延迟队列,如30分钟
  3. 30分钟后,该消息没有任何人消费,成为死信,成为死信后转发到订单队列
  4. 订单队列接收到转发过来的死信消息,很快就被消费者消费了

订单超时生产者配置类:

package com.cc.config.provider;

import com.cc.config.RabbitConstant;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

/**
 * 订单超时消息
 * 需有两个队列:订单延迟队列和订单队列
 * 发送订单超时消息的流程为:
 * 1. 发送消息到订单延迟队列,并指定过期时间
 * 2. 订单延迟消息过期后成为死信,并转发到订单队列中
 * 3. 订单队列收到订单延迟队列转发过来的消息,表示此时该订单消息已超时
 * @author cc
 * @date 2021-11-27 14:46
 */
@Configuration
public class OrderTimeoutRabbitConfig {
    @Bean
    public Queue orderDelayQueue() {
        Map<String, Object> params = new HashMap<>();
        // 声明,当队列里的消息过期后(即死信)转发到哪个Exchange
        params.put("x-dead-letter-exchange", RabbitConstant.OrderExchange);
        // 声明,死信转发时携带的routing-key
        params.put("x-dead-letter-routing-key", RabbitConstant.OrderRouting);
        return new Queue(RabbitConstant.OrderDelayQueue, true, false, false, params);
    }

    @Bean
    public DirectExchange orderDelayExchange() {
        return new DirectExchange(RabbitConstant.OrderDelayExchange);
    }

    @Bean
    public Binding orderDelayBinding() {
        return BindingBuilder.bind(orderDelayQueue()).to(orderDelayExchange()).with(RabbitConstant.OrderDelayRouting);
    }

    /**
     * 要区分开订单延迟队列和订单队列
     * 订单延迟队列的目的是为了实现延时
     * 订单队列的目的是订单超时后的提醒
     */

    @Bean
    public Queue orderQueue() {
        return new Queue(RabbitConstant.OrderQueue, true);
    }

    @Bean
    public TopicExchange orderExchange() {
        return new TopicExchange(RabbitConstant.OrderExchange);
    }

    @Bean
    public Binding orderBinding() {
        return BindingBuilder.bind(orderQueue()).to(orderExchange()).with(RabbitConstant.OrderRouting);
    }
}

模拟一个订单类:

package com.cc.config.model;

import java.io.Serializable;

/**
 * 订单类
 * @author cc
 * @date 2021-11-27 15:24
 */
public class Order implements Serializable {
    /**
     * 订单id
     */
    private String orderId;

    /**
     * 订单状态
     */
    private Integer orderStatus;
	...
}

消费者消费类:

package com.cc.config.consumer;

import com.cc.config.RabbitConstant;
import com.cc.config.model.Order;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.util.Map;

/**
 * 直连交换机消费者
 * @author cc
 * @date 2021-11-27 10:55
 */
@Component
@RabbitListener(queues = RabbitConstant.OrderQueue)
public class OrderTimeoutReceiver {
    @RabbitHandler
    public void process(Order order) {
        Integer status = order.getOrderStatus();
        if (status == 0) {
            System.out.println("订单未支付,关闭订单,释放库存");
        } else {
            System.out.println("订单已支付。");
        }
        System.out.println("收到订单超时的消息: " + order.toString());
    }
}

写接口测试:

// 延时队列
@GetMapping("/sendDelayMsg")
public String sendDelayMsg() {
    for (int i = 0; i < 100; i++) {
        Order order = new Order();
        order.setOrderId(UUID.randomUUID().toString().replace("-", ""));
        order.setOrderStatus(new Random().nextInt(2));
        rabbitTemplate.convertAndSend(RabbitConstant.OrderDelayExchange, RabbitConstant.OrderDelayRouting, order, new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                message.getMessageProperties().setExpiration(2000 + "");
                return message;
            }
        });
    }

    return "ok";
}

测试结果为,消息发送2秒后被消费。

消费过程出现异常如何处理

因为消息被消费后默认是自动确认的,所以会出现在消费过程中出现异常后,业务没有处理成功,消息也没了的情况。

这时候我们就需要手动确认消息,当过程出现异常,就将消息放回队列重新消费。

我们可以指定需要手动确认的队列:

手动确认消息配置类:

package com.cc.config.consumer.confirm;

import com.cc.config.RabbitConstant;
import org.springframework.amqp.core.AcknowledgeMode;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 消息接收,手动确认
 * @author cc
 * @date 2021-11-27 14:33
 */
@Configuration
public class MessageListenerConfig {
    private final CachingConnectionFactory connectionFactory;
    private final MyAckReceiver myAckReceiver;

    public MessageListenerConfig(CachingConnectionFactory connectionFactory, MyAckReceiver myAckReceiver) {
        this.connectionFactory = connectionFactory;
        this.myAckReceiver = myAckReceiver;
    }

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

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

        return container;
    }
}

手动确认消息类:

package com.cc.config.consumer.confirm;

import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;

import java.util.Random;

/**
 * 消息手动确认监听类,手动确认模式需要实现ChannelAwareMessageListener
 *
 * @author cc
 * @date 2021-11-27 11:28
 */
@Component
public class MyAckReceiver implements ChannelAwareMessageListener {
    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
//            if (RabbitConstant.DirectQueue.equals(message.getMessageProperties().getConsumerQueue())) {
//                System.out.println("消费消息来自的队列名: " + message.getMessageProperties().getConsumerQueue());
//                System.out.println("执行 DirectQueue 中的消息业务处理流程");
//            }
//
//            if (RabbitConstant.FanoutA.equals(message.getMessageProperties().getConsumerQueue())) {
//                System.out.println("消费消息来自的队列名: " + message.getMessageProperties().getConsumerQueue());
//                System.out.println("执行 FanoutA 中的消息业务处理流程");
//            }

            System.out.println("消费消息来自: " + message.getMessageProperties().getConsumerQueue());

            if (new Random().nextInt(2) == 0) {
                // 模拟异常
                int a = 1 / 0;
            }

            channel.basicAck(deliveryTag, true);
        } catch (Exception e) {
            // 为true会重新放回队列
            System.out.println("消息处理出现异常,将消息放回队列中");
//            channel.basicReject(deliveryTag, true);
            channel.basicNack(deliveryTag, true, true);
            e.printStackTrace();
        }
    }
}

在示例中,我们用刚刚的订单超时的队列来做示范,在消费过程中模拟随机异常,测试结果为,当消费过程异常,消息被重新放入队列中重新消费,直到消费顺利,当然在实际场景中要增加重复消费次数的判断。

手动确认消息的几个知识点,以下知识点来源于第四章----SpringBoot+RabbitMQ发送确认和消费手动确认机制

  • channel.basicAck(long, boolean):确认收到消息,消息将被队列移除,false只确认当前consumer一个消息收到,true确认所有consumer获得的消息
  • channel.basicNack(long, boolean, boolean):否定消息,第一个boolean表示一个consumer还是所有,第二个boolean表示是否重新回到队列,true重新入队
  • channel.basicReject(long, boolean):拒绝消息,boolean为false表示不再重新入队,如果配置了死信队列则进入死信队列
  • 当消息回滚到消息队列时,该消息不会回到队列尾部,而且仍在队列头部,这是消费者又会马上接收到这条消息,如果想要消息进入队尾,须确认后再次发送消息