SpringCloud —— RabbitMQ消息队列详解

一、前言

对于黑马商城的支付微服务部分的处理,我们目前是全部使用的同步调用,也就是直接远程调用其他微服务,同步调用的优点有很多,比如稳定性高,及时性强,但是在高并发的场景下,如果所有业务模块全部使用同步调用,就会对服务器造成压力,从而影响用户体验(卡),基于这个问题,我们会选择让一些不太需要及时性,并且不是核心业务功能的模块采用异步调用,而实现异步调用需要使用到消息队列,大致的作用就是在远程调用前用一个队列进行消息的分发,当微服务想进行远程调用时,只需要将消息传给消息队列即可,传给MQ(消息队列)后,就继续执行代码,不用再去关注消息是否传递,或者传递给谁了,这些步骤都交给MQ来实现了。

二、RabbitMQ

1.部署和基本概念辨析

对于消息队列的组件,我们还是选择直接部署在docker中:

bash 复制代码
docker run \
 -e RABBITMQ_DEFAULT_USER=itheima \
 -e RABBITMQ_DEFAULT_PASS=123321 \
 -v mq-plugins:/plugins \
 --name mq \
 --hostname mq \
 -p 15672:15672 \
 -p 5672:5672 \
 --network hm-net\
 -d \
 rabbitmq:3.8-management

在项目中导包:

XML 复制代码
        <!--AMQP依赖,包含RabbitMQ-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

在项目中配置rabbitmq:

bash 复制代码
spring:
  rabbitmq:
    host: 192.168.111.111 # 你的虚拟机IP
    port: 5672 # 端口
    virtual-host: /hmall # 虚拟主机
    username: hmall # 用户名
    password: 12345 # 密码
    listener:
      simple:
        prefetch: 1

首先要辨析清楚以下的概念,整个消息队列实际上是由交换机和队列两部分组成,交换机负责分发消息给队列(这里会按照一定规则,可能是通配符,也可能是直接广播或者绑定),队列负责传递消息。

生产者是消息的发起人,也就是进行远程调用的微服务,而消费者是被远程调用的微服务,在我们的支付流程中,支付微服务就是生产者,而交易微服务就是消费者。

虚拟主机是为了让各个队列和交换机隔离,其实也就是用户,用户只能操纵自己的交换机和队列,这样就保障的每个项目的消息队列的独立性。

  • publisher:生产者,也就是发送消息的一方

  • consumer:消费者,也就是消费消息的一方

  • queue:队列,存储消息。生产者投递的消息会暂存在消息队列中,等待消费者处理

  • exchange:交换机,负责消息路由。生产者发送的消息由交换机决定投递到哪个队列。

  • virtual host:虚拟主机,起到数据隔离的作用。每个虚拟主机相互独立,有各自的exchange、queue

现在我们只需要使用rabbitmq的网页控制台来操作即可:

2.快速入门

我们在控制台手动创建一个simple队列:

先看看项目的结构,我们这里使用两个模块来模拟生产者和消费者:

消费者监听队列,一旦收到队列传递过来的消息就直接打印日志:

java 复制代码
  @RabbitListener(queues = "simple.queue")
    public void listenSimpleQueue(String message) {
        log.info("监听到simple.queue的消息:{}", message);
    }

在生产者中,我们使用测试代码生产消息,尝试向队列中传递一个message字符串:

首先要自动装配RabbitTemplate(之前已经导了包了),使用模板对象的convertAndSend方法来将指定消息传递到指定的队列中去。

java 复制代码
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void testSimpleQueue() {
        //1.队列名
        String queueName = "simple.queue";
        //2.消息
        String message = "hello spring amqp!!!";
        //3.发送消息
        rabbitTemplate.convertAndSend(queueName, message);

    }

在我们启动消费者后,运行测试方法,就可以得到:

说明消费者拿到消息了,这个时候就可以随意处理消息了。

3.交换机

前面提到了,交换机负责分发消息给队列(这里会按照一定规则,可能是通配符,也可能是直接广播或者绑定)。

所以显然的,交换机会有很多种类,不同的分发规则就要使用不同的交换机。

交换机绑定的队列默认是按照轮询规则来传递消息的,也就是说,假如有两个队列,同时广播消息进去,队列1会被分到消息0、2、4、6......,队列2会被分到消息1、3、5、7......

这样会出现问题,假如一个队列的处理速度很慢,另一个队列处理得很快,那么就会导致快队列等待慢队列处理,直到慢队列处理完了一条,快队列才处理下一条。

所以我们在配置文件中会写一条:

bash 复制代码
listener:
      simple:
        prefetch: 1

让分发规则改为:处理完一条消息马上处理下一条,这样就不会等待其他队列了,能者多劳,这样的规则才更符合实际生产。

(1)fanout交换机

我们在这里将写出完整的代码和过程,后面其他种类的交换机可以同理可得。

fanout交换机负责广播消息,也就是只要和fanout绑定的所有队列,都要接收发给fanout的所有消息。

首先创建这个交换机:

然后创建队列:

将交换机和两个队列绑定:

接下来就开始写代码了:

消费者监听队列消息:

java 复制代码
    //fanout交换机
    @RabbitListener(queues = "fanout.queue1")
    public void listenFanoutQueue1(String message) throws InterruptedException {
        log.info("消费者1监听到 fanout.queue1的消息:{}", message);
    }

    @RabbitListener(queues = "fanout.queue2")
    public void listenFanoutQueue2(String message) throws InterruptedException {
        log.info("消费者2监听到 fanout.queue2的消息:{}", message);
    }

测试广播的代码,将消息发给交换机(中间的 "null" 是路由,这里是广播,所以直接空着就是了)

java 复制代码
@Test
    public void testFanoutQueue() {
        //1.交换机名
        String exchange = "hmall.fanout";

        //2.消息
        String message = "hello everyone!!!";

        //3.发送消息
        rabbitTemplate.convertAndSend(exchange, "null", message);
    }

测试:

可以看到所有的消费者都拿到了消息,说明是广播成功了的。

(2)direct交换机

这个交换机负责定向传递消息,所以刚刚我们空着的路由,要在这里写出来了。

这个时候绑定队列时就要填路由了,运行测试代码后交换机会向指定路由的队列发送指定消息。

java 复制代码
    //direct交换机
    @RabbitListener(queues = "direct.queue1")
    public void listenDirectQueue1(String message) throws InterruptedException {
        log.info("消费者1监听到 direct.queue1的消息:{}", message);
    }

    @RabbitListener(queues = "direct.queue2")
    public void listenDirectQueue2(String message) throws InterruptedException {
        log.info("消费者2监听到 direct.queue2的消息:{}", message);
    }

测试一下:

java 复制代码
 @Test
    public void testDirectQueue() {
        //1.交换机名
        String exchange = "hmall.direct";

        //2.消息
        String message = "hello red!!!";
        String message2 = "hello blue!!!";
        String message3 = "hello yellow!!!";

        //3.发送消息
        rabbitTemplate.convertAndSend(exchange, "red", message);
        rabbitTemplate.convertAndSend(exchange, "blue", message2);
        rabbitTemplate.convertAndSend(exchange, "yellow", message3);
    }

这样就根据不同的路由来传递消息了:

(3)topic交换机

按照通配符规则来分发消息:

java 复制代码
    //Topic交换机
    @RabbitListener(queues = "topic.queue1")
    public void listenTopicQueue1(String message) throws InterruptedException {
        log.info("消费者1监听到 topic.queue1的消息:{}", message);
    }

    @RabbitListener(queues = "topic.queue2")
    public void listenTopicQueue2(String message) throws InterruptedException {
        log.info("消费者2监听到 topic.queue2的消息:{}", message);
    }

测试代码:

java 复制代码
    @Test
    public void testTopicQueue() {
        //1.交换机名
        String exchange = "hmall.topic";

        //2.消息
        String message = "hello world!!!";
        String message2 = "yds news!!!";
        String message3 = "china nb!!!";

        //3.发送消息
        rabbitTemplate.convertAndSend(exchange, "china.news", message);
        rabbitTemplate.convertAndSend(exchange, "yds.news", message2);
        rabbitTemplate.convertAndSend(exchange, "china.yyds", message3);

    }

测试结果:

4.脱离控制台声明

(1)Bean声明

对于刚刚我们在控制台创建的几个关于fanout交换机的队列,我们可以在一个配置类中声明,当项目启动时,将自动创建这些队列、交换机,以及他们的绑定关系。

java 复制代码
//用Bean声明交换机和队列及绑定
@Configuration
public class FanoutConfiguration {

    @Bean
    public FanoutExchange fanoutExchange() {
        //return new FanoutExchange("hmall.fanout");
        return ExchangeBuilder.fanoutExchange("hmall.fanout").build();
    }

    @Bean
    public Queue fanoutQueue1() {
        //return new Queue("fanout.queue1");
        return QueueBuilder.durable("fanout.queue1").build();
    }

    @Bean
    public Binding fanoutQueue1Binding(Queue fanoutQueue1, FanoutExchange fanoutExchange) {
        return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
    }

    @Bean
    public Queue fanoutQueue2() {
        //return new Queue("fanout.queue1");
        return QueueBuilder.durable("fanout.queue2").build();
    }

    @Bean
    public Binding fanoutQueue2Binding(Queue fanoutQueue2, FanoutExchange fanoutExchange) {
        return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
    }

}

无论是Queue类、Exchange类还是Binding类,都是基于AMQP包中的,所以使用前需要导包(当然,我们前面已经导入过了)。

(2)注解声明

在刚刚我们创建监听器类的时候,我们就使用到了@RabbitListener注解,当时我们仅仅是用这个注解来标记监听的队列名。

其实这个注解还可以用来声明队列和交换机,下面我们尝试使用注解来声明direct交换机:

java 复制代码
    //direct交换机边监听边声明(注解声明)
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue("direct.queue1"),
            exchange = @Exchange(name = "hmall.direct",type = ExchangeTypes.DIRECT),
            key = {"red","blue"}
    ))
    public void listenDirectQueue1(String message) throws InterruptedException {
        log.info("消费者1监听到 direct.queue1的消息:{}", message);
    }
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue("direct.queue2"),
            exchange = @Exchange(name = "hmall.direct",type = ExchangeTypes.DIRECT),
            key = {"red","yellow"}
    ))
    public void listenDirectQueue2(String message) throws InterruptedException {
        log.info("消费者2监听到 direct.queue2的消息:{}", message);
    }

注解最方便的一点就是对于路由key的配置,我们不需要一个一个的去绑定了,这里可以直接用字符串数组来定义路由,如果不使用注解,我们的配置类将会很冗长:

java 复制代码
@Configuration
public class DirectConfiguration {

    @Bean
    public DirectExchange directExchange() {
        //return new FanoutExchange("hmall.fanout");
        return ExchangeBuilder.directExchange("hmall.direct").build();
    }

    @Bean
    public Queue directQueue1() {
        return QueueBuilder.durable("direct.queue1").build();
    }

    @Bean
    public Binding directQueue1BindingRed(Queue directQueue1, DirectExchange directExchange) {
        return BindingBuilder.bind(directQueue1).to(directExchange).with("red");
    }

    @Bean
    public Binding directQueue1BindingBlue(Queue directQueue1, DirectExchange directExchange) {
        return BindingBuilder.bind(directQueue1).to(directExchange).with("blue");
    }


    @Bean
    public Queue directQueue2() {
        return QueueBuilder.durable("direct.queue2").build();
    }

    @Bean
    public Binding directQueue2BindingRed(Queue directQueue2, DirectExchange directExchange) {
        return BindingBuilder.bind(directQueue2).to(directExchange).with("red");
    }

    @Bean
    public Binding directQueue2BindingYellow(Queue directQueue2, DirectExchange directExchange) {
        return BindingBuilder.bind(directQueue2).to(directExchange).with("yellow");
    }

}

5.消息转换器

如果我们默认传一个消息,队列中的字符串将以序列化的形式写进去,我们进行测试,将如下的键值对传入队列:

java 复制代码
@Test
    public void testSendObject() {
        //1.准备消息
        Map<String, Object> msg = new HashMap<>(2);
        msg.put("name", "yds");
        msg.put("age", "20");

        //2.发送消息
        rabbitTemplate.convertAndSend("object.queue", msg);

    }

传入后将看到如下信息,可以看到我们的键值对在这里是被序列化了,这是JDK自带的,不易阅读,而且占用的字节数较大,我们自然会想到使用更加直观常用的JSON格式了,那这就会涉及到消息的转换了。

要使用转换器,我们首先要先配置消息转换器,先导入JSON转换器的包:

XML 复制代码
         <!--jackson-->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>

然后编写配置类:(这里偷懒了,就直接在启动类中注册Bean了)

java 复制代码
@SpringBootApplication
public class ConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class, args);
    }

    //消息转化器
    @Bean
    public MessageConverter messageConverter(){
        return new Jackson2JsonMessageConverter();
    }
}

注意:消费者和生产者都需要配置消息转化器。

最后写一个监听器来监听队列:

java 复制代码
    //消息转换器
    @RabbitListener(queues = "object.queue")
    public void listenObjectQueue(Map<String,Object> message) {
        log.info("object.queue的消息:{}", message);
    }

运行测试类,成功用JSON格式获取消息:

三、商城业务改造

首先要明确什么业务可以进行改造,我们发现,在支付微服务中将远程调用交易微服务,然后使用交易微服务对订单的支付状态进行更新,这一步既不是支付的核心步骤,也不需要很高的及时性,我们选择将这个远程同步调用改为异步调用。

(1)改业务

可以看到,先前远程调用交易微服务是在第五步:

现在我们改成消息队列的异步调用:

java 复制代码
@Override
    @GlobalTransactional
    public void tryPayOrderByBalance(PayOrderFormDTO payOrderFormDTO) {
        // 1.查询支付单
        PayOrder po = getById(payOrderFormDTO.getId());
        // 2.判断状态
        if(!PayStatus.WAIT_BUYER_PAY.equalsValue(po.getStatus())){
            // 订单不是未支付,状态异常
            throw new BizIllegalException("交易已支付或关闭!");
        }
        // 3.尝试扣减余额
        userClient.deductMoney(payOrderFormDTO.getPw(), po.getAmount());
        // 4.修改支付单状态
        boolean success = markPayOrderSuccess(payOrderFormDTO.getId(), LocalDateTime.now());
        if (!success) {
            throw new BizIllegalException("交易已支付或关闭!");
        }
        //TODO 5.修改订单状态
        //tradeClient.markOrderPaySuccess(po.getBizOrderNo());
        try {

            rabbitTemplate.convertAndSend("pay.direct","pay.success",po.getBizOrderNo());
        } catch (Exception e){
            log.info("发送支付状态通知失败,订单id:{}",po.getBizOrderNo());
        }

    }

(2)MQ相关配置

在这之前,记得导包和配置MQ,我这里直接写了个共享配置进去,所以只需要在bootstrap中新增一个共享配置id即可:

导包:

XML 复制代码
        <!--amqp-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

配置:

java 复制代码
spring:
  application:
    name: pay-service
  profiles:
    active: dev
  cloud:
    nacos:
      server-addr: 192.168.242.130:8848
      config:
        file-extension: yaml
        shared-configs:
          - data-id: shared-jdbc.yaml
          - data-id: shared-log.yaml
          - data-id: shared-swagger.yaml
          - data-id: shared-seata.yaml
          - data-id: shared-mq.yaml

至于传入的消息格式嘛,还是使用JSON,所以别忘了配置消息转换器:

java 复制代码
@Configuration
public class MqConfig {

    @Bean
    public MessageConverter messageConverter(){
        return new Jackson2JsonMessageConverter();
    }

}

我们在这里将消息转换器写到了hm-common中,因为几乎每个微服务都要用到这个消息转换器。

别忘了在spring.factories中去配置配置类(由于hm-common中没有启动类,所以无法自动识别配置类),否则配置无法生效。

(3)MQ声明和监听器

在交易微服务中创建监听器,专门用来修改订单的支付状态,同时声明MQ:

java 复制代码
@Component
@RequiredArgsConstructor
public class PayStatusListener {

    private final IOrderService orderService;

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "trade.pay.success.queue",durable = "true"),
            exchange = @Exchange(name = "pay.direct"),
            key = "pay.success"
    ))
    public void listenPaySuccess(Long orderId){
        orderService.markOrderPaySuccess(orderId);
    }
}
相关推荐
面对疾风叭!哈撒给2 小时前
Liunx之Docker 安装启动 influxdb2
java·spring cloud·docker
清晓粼溪3 小时前
SpringCloud-05-Micrometer Tracing+ZipKin分布式链路追踪
分布式·spring·spring cloud
独自破碎E3 小时前
聊聊RabbitMQ
分布式·rabbitmq
qq_381454993 小时前
Java与RabbitMQ:异步通信黄金组合
java-rabbitmq
梵得儿SHI3 小时前
SpringCloud 核心组件精讲:Sentinel 熔断限流全攻略-流量控制、熔断降级、热点参数限流(含 Dashboard 部署 + 项目集成实操)
java·spring cloud·sentinel·熔断降级·热点参数限流·微服务流量控制
麦兜*3 小时前
Spring Boot 3.x 升级踩坑大全:Jakarta EE 9+、GraalVM Native 与配置迁移实战
java·spring boot·后端·spring·spring cloud
wang09073 小时前
支持rabbitmq多数据源
rabbitmq
indexsunny3 小时前
互联网大厂Java面试实战:Spring Cloud微服务与Redis缓存在电商场景中的应用
java·spring boot·redis·spring cloud·微服务·消息队列·电商
冷雨夜中漫步4 小时前
Spring Cloud入门—— (1)Spring Cloud Alibaba生态组件Nacos3.0本地部署
后端·spring·spring cloud
indexsunny4 小时前
互联网大厂Java面试实录:从Spring Boot到微服务实战解析
java·spring boot·spring cloud·kafka·microservices·java interview·software development