RabbitMQ基础

RabbitMQ

使用背景

在微服务项目中,由于服务进行了拆分,必然会涉及到不同服务之间的相互调用,而在调用中发起请求需要等待服务器执行业务返回结果,才能继续执行后面的业务,也就是说在等待过程中是处于阻塞状态,因此我们将这种调用方式称为同步调用,也可以将做同步通讯。

而在很多场景中我们会使用异步通讯

  • 同步通讯:双方的交互是实时的,就像打电话一样,同一时间,你只能跟一个人打视频电话
  • 异步通讯:双方的交互不是实时的,不需要立刻给对方回应,因此在异步通讯下,可以多线操作,同时跟多个人聊天

两种方式各有优势,但是在我们的业务中,如果需要实时得到响应,则应该选择同步通讯,而如果追求更高的效率,而且不需要实时响应,则应该选择异步通讯

在之前我们同步调用是采用OpenFeign调用

而本篇文章学习异步调用采用RabbitMQ调用

异步调用

异步调用的方式其实就是基于消息通知的方式,一般包含三个角色

  • 消息发送者:投递消息的人,就是原来的调用方
  • 消息Broker:管理,暂存,转发消息,你可以把它理解成微信服务器
  • 消息接收者:接收和处理消息的人

在异步调用中,发送者不再直接同步调用接收者的业务接口,而是发送一条消息投递给消息Broker。然后接收者根据自己的需求从消息Broker那里订阅消息。每当发送方发送消息后,接收者都能获取消息并处理。

这样就能实现,发送消息的人和接收消息的人就完成解耦

优点

  1. 耦合度更低
  2. 性能更好
  3. 业务拓展性强
  4. 故障隔离,避免级联失败

异步通信也有它的缺点;

  1. 完全依赖于Broker的可靠性,安全性,和性能
  2. 架构复杂,后期维护和调试麻烦

RabbitMQ架构

  • publisher:生产者,也就是发送消息的一方
  • consumer:消费者,也就是消费消息的一方
  • queue:队列,存储消息。生产者投递的消息会暂存在消息队列中,等待消费者处理
  • exchange:交换机,负责消息路由。生产者发送的消息由交换机决定投递到哪个队列。
  • virtual host:虚拟主机,起到数据隔离的作用。每个虚拟主机相互独立,有各自的exchange、queue

收发消息

  • 交换机
  • 队列

安装

我们如果想要使用MQ,则需要在环境上安装MQ

基于Docker来安装RabbitMQ

docker run \
 -e RABBITMQ_DEFAULT_USER=itcaca \
 -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

15672:MQ提供的管理控制台的端口

2672:MQ的消息发送处理接口

安装完成后,我们需要访问 http://xxxxxxx:15672就可以进入管理控制台,首次访问需要登录,默认的用户名和密码已经在安装命令中已经指定了(itcaca/123321)

收发架构

概念:

  • publisher:生产者,也就是发送消息的一方
  • consumer:消费者,也就是消费消息的一方
  • queue:队列,存储消息。生产者投递的消息会暂存在消息队列中,等待消费者处理
  • exchange:交换机,负责消息路由。生产者发送的消息由交换机决定投递到哪个队列。
  • virtual host:虚拟主机,起到数据隔离的作用。每个虚拟主机相互独立,有各自的exchange、queue

流程

我们发送到交换机的消息,只会路由到与其绑定的队列,因此我们创建完队列之后,我们需要将其与其交换机绑定

Spring AMQP

在业务开发中,我们不会在控制台收发消息,而是应该基于编程的方式,由于RabbitMQ采用了AMQP协议,因此他具备跨语言的特性,任何语言只要遵循AMQP协议收发消息,都可以与RabbitMQ交互,并且RabbitMQ官方也提供了各种不同语言的客户端

Spring官方基于RabbitMQ提供了这样一套消息收发的模板工具:SpringAMQP。并且还基于SpringBoot对其实现了自动装配。

Spring AMQP提供了三个功能

  • 自动声明队列,交换机及其绑定关系
  • 基于注解的监听器模式,异步接收消息
  • 封装了RabbitTemplate工具,用于发送消息

依赖

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

入门

MQ的流程应该是将消息发送到交换机然后,交换机发送消息到绑定的队列,在以下实例代码中,我们直接跳过交换机,向队列发送消息。

在使用之前,我们先配置MQ地址,在yml文件中添加配置

spring:
  rabbitmq:
    host: # 你的虚拟机IP
    port: 5672 # 端口
    virtual-host: /hmall # 虚拟主机
    username: hmall # 用户名
    password: 123 # 密码

发送消息

publisher服务中编写测试类,并利用RabbitTemplate实现消息发送

package com.itheima.publisher.amqp;

import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class SpringAmqpTest {

    @Autowired
    private RabbitTemplate rabbitTemplate;

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

接收消息

接收消息也需要首先配置MQ地址,在yml中添加配置

spring:
  rabbitmq:
    host:# 你的虚拟机IP
    port: 5672 # 端口
    virtual-host: /hmall # 虚拟主机
    username: hmall # 用户名
    password: 123 # 密码

consumer服务中编写消息监听器类,并利用@RabbitListener实现消息接收消费

package com.itheima.consumer.listener;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class SpringRabbitListener {

    /*
    利用@RabbitListener注解,可以监听到对应队列的消息
    一旦监听的队列有消息,就会回调当前方法,在方法中接收消息并消费处理消息
     */
    @RabbitListener(queues = "simple.queue")
    public void listenSimpleQueueMessage(String message) throws Exception {
        System.out.println("SpringRabbitListener listenSimpleQueueMessage 消费者接收到消息: " + message);
    }
}

测试

启动consumer服务,然后在publisher服务运行测试代码,发送MQ消息。

WorkQueue模型

Work queues,任务模型。简单来说就是让多个消费者绑定到一个队列,共同消费队列中的消息。

这就产生了问题

问题应用场景

当消息处理比较耗时的时候,可能生产消息的速度会远远大于消息的消费速度。长此以往,消息就会堆积越来越多,无法及时处理。

此时就可以使用work模型,多个消费者共同处理消息处理,消息处理的速度就能大大提高了

正常情况下,消息平均分配到每个消费者,并没有考虑到消费者的处理能力,导致1个消费者空闲,另一个消费者忙到不可开交,没有充分利用每一个消费者的能力,最终消息处理的耗时远远超过了1秒。

能者多劳配置

在Spring中可以这样配置,可以使每次只能获取一条消息,处理完成才能获取下一个消息

spring:
  rabbitmq:
    listener:
      simple:
        prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息

这样配置可以使两个消费者充分利用他们的处理能力,可以有效避免消息积压问题。

Work模型的使用

  • 多个消费者绑定到一个队列,同一个消费只会被一个消费者处理
  • 通过设置prefetch来控制消费者预取的消息数量

交换机类型

下面我们引入交换机。

注意,Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失。

交换机有四种类型

  1. Fanout:广播,将消息交给所有绑定到交换机的队列。我们最早在控制台使用的正是Fanout交换机。
  2. Direct:订阅,基于RoutingKey发送给订阅了消息的队列
  3. Topic:通配符订阅,与Direct类似,只不过RoutingKey可以使用通配符
  4. Headers:头匹配,基于MQ的消息头匹配,用的较少

Fanout交换机

Fanout,英文翻译是扇出,我觉得在MQ中叫广播更合适。

  1. 可以有多个队列
  2. 每个队列都要绑定到Exchange(交换机)
  3. 生产者发送的消息,只能发送到交换机
  4. 交换机把消息发送给绑定过的所有队列
  5. 订阅队列的消费者都能拿到消息

场景

  • 创建一个名为 hmall.fanout的交换机,类型是Fanout
  • 创建两个队列fanout.queue1fanout.queue2,绑定到交换机hmall.fanout

消息发送

在publisher服务的SpringAmqpTest类中添加测试方法

    /*
    测试 fanout exchange;
    向 hmall.fanout 交换机发送消息,消息内容为 hello everyone!,会发送到所有绑定到该交换机的队列
     */
    @Test
    public void testFanoutExchange() {
        //交换机名称
        String exchangeName = "hmall.fanout";
        //发送的内容
        String message = "hello everyone!";
        //发送消息
        rabbitTemplate.convertAndSend(exchangeName, "", message);
    }

convertAndSend方法的第二个参数,路由key由于没有绑定,所以可以指定为空

消息接收

在consumer服务的SpringRabbitListener中添加两个办法,作为消费者:

    /*
    监听 fanout.queue1 队列的消息
     */
    @RabbitListener(queues = "fanout.queue1")
    public void listenFanoutQueue1(String message) {
        System.out.println("【消费者1】接收到消息: " + message );
    }
    /*
    监听 fanout.queue2 队列的消息
     */
    @RabbitListener(queues = "fanout.queue2")
    public void listenFanoutQueue2(String message) {
        System.out.println("【消费者2】接收到消息: " + message );
    }

交换机的作用:

  • 接收publisher发送的消息
  • 将消息按照规则路由到与之绑定的队列
  • 不能缓存消息,路由失败,消息丢失
  • FanoutExchange的会将消息路由到每个绑定的队列

Direct交换机

在Fanout模式中,一条消息,会被所有订阅的队列都消费,但是,在某些场景下,我们希望不同的消息被不同的队列消费,这时就要用到Direct类型的Exchange。

在Direct模型下:

  1. 队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key)
  2. 消息的发送方法在向Exchange发送消息时,也必须指定消息的RoutingKey
  3. Exchange不再把消息交给每一个绑定的队列,而是根据消息的Routing key进行判断,只有队列的Routing Key与消息的Routing Key完全一致,才会接收到消息

场景

  • 声明一个名为hmall.direct的交换机
  • 声明队列direct.queue1,绑定hmall.direct,bingdingKey为blud和red
  • 声明队列direct.queue2,绑定hmall.direct,bindingKey为yellow和red
  • 在consumer服务中,编写两个消费者方法,分别监听direct.queue1和direct.queue2
  • 在publisher中编写测试方法,向hmall.direct发送消息

消息发送

在publisher服务的SpringAmqpTest类中添加测试方法:

/*
测试 direct exchange;
向 hmall.direct 交换机发送消息,会根据路由key发送到所有绑定到该交换机的队列
 */
@Test
public void testDirectExchange() {
    String exchangeName = "hmall.direct";
    String message = "震惊!哈尔滨上空惊现黑龙,吞云吐雾喜迎八方来客!";
    //发送 路由key 为 red 的消息;
    rabbitTemplate.convertAndSend(exchangeName, "red", message);

    //发送 路由key 为blue的消息;
    message = "最新消息!哈尔滨上空的黑龙实为纸鸢,已送给来尔滨的公主及殿下。";
    rabbitTemplate.convertAndSend(exchangeName, "blue", message);
}

由于hmall.redirect交换机绑定的两个队列的路由key有red;所以指定了路由key为red的消息能被两个消费者都收到。

而路由key为blue的队列只有direct.queue1;所以只有监听这个队列的消费者1能够接收到消息;

消息接收

在consumer服务的SpringRabbitListener中添加方法:

    /*
    监听 direct.queue1 队列的消息
     */
    @RabbitListener(queues = "direct.queue1")
    public void listenDirectQueue1(String message) {
        System.out.println("【消费者1】接收到消息: " + message );
    }
    /*
    监听 direct.queue2 队列的消息
     */
    @RabbitListener(queues = "direct.queue2")
    public void listenDirectQueue2(String message) {
        System.out.println("【消费者2】接收到消息: " + message );
    }

Direct交换机和Fanout交换机的差异

  • Fanout交换机将消息路由给每一个与之绑定的队列
  • Direct交换机根据RoutingKey判断路由给哪个队列
  • 如果多个队列具有相同的RoutingKey,则于Fanout功能类似

Topic交换机

概述

Topic类型的Exchange与Ditect相比,都是可以根据RoutingKey把消息路由到不同的队列

只不过Topic类型Exchange可以让队列在绑定RoutingKey的时候使用通配符。

RoutingKey一般都是有一个或多个单词组成,多个单词之间以.分割,列如:item.insert

通配符规则

  • #:匹配一个或多个词
  • *:匹配恰好一个词

列如

  • item.#可以匹配item.spu.insert或者item.spu
  • item.*只能匹配item.spu

场景

publicsher发送的消息使用的RoutingKey共有四种:

  • china.news代表有中国的新闻消息
  • china.weather代表中国的天气消息
  • japan.news则代表日本新闻
  • japan.weather代表日本的天气消息

topic.queue1:绑定的是china.#,凡是以china.开头的routing key 都会被匹配到。包括:

  • china.news
  • china.weather

topic.queue2:绑定的是#.news,凡是以.news结尾的routing key 都会被匹配,包括:

  • china.news
  • japan.news

消息发送

在publisher服务的SpringAmqpTest类中添加测试方法

/*
    测试 topic exchange;
    向 hmall.topic 交换机发送消息,路由key为china.news 的消息
     */
    @Test
    public void testTopicExchange() {
        String exchangeName = "hmall.topic";
        String message = "中国冰城哈尔滨在这个冬天旅游火爆!";
        //发送 路由key 为 china.news 的消息;
        rabbitTemplate.convertAndSend(exchangeName, "china.news", message);
    }

消息接收

在consumer服务的SpringRabbitListener中添加方法

    /*
    监听 topic.queue1 队列的消息
     */
    @RabbitListener(queues = "topic.queue1")
    public void listenTopicQueue1(String message) {
        System.out.println("【消费者1】接收到消息: " + message );
    }   
    /*
    监听 topic.queue2 队列的消息
     */
    @RabbitListener(queues = "topic.queue2")
    public void listenTopicQueue2(String message) {
        System.out.println("【消费者2】接收到消息: " + message );
    }   

Direct交换机与Topic交换机的差异

  • Topic交换机接收的消息RoutingKey必须是多个单词,以.分割
  • Topic交换机与队列绑定时的RoutingKey可以指定通配符
  • #:代表0个或者多个词
  • *:代表1个词

代码声明队列和交换机

基本API

SpringAMQP提供了一个Queue类,用来创建队列

public class Queue extends AbstractDeclarable implements Cloneable{}

SpringAMQP还提供了一个Exchange接口,来表示所有不同类型的交换机,我们可以自己创建队列和交换机,不过SpringAMQP还提供了ExchangeBuilder来简化这个过程。

在绑定队列和交换机的时候,需要BindingBuilder来创建Binding对象

fanout示例

在consumer服务中创建一个配置类,FanoutConfig,声明队列和交换机

package com.itheima.consumer.config;

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;

@Configuration
public class FanoutConfig {
    /*
    声明fanout类型交换机
     */
    @Bean
    public FanoutExchange fanoutExchange() {
        return new FanoutExchange("hmall.fanout");
    }

    /*
    声明队列,名称为 fanout.queue1
     */
    @Bean
    public Queue fanoutQueue1() {
        return new Queue("fanout.queue1");
    }
    /*
    绑定队列和交换机
     */
    @Bean
    public Binding fanoutBinding1(Queue fanoutQueue1, FanoutExchange fanoutExchange) {
        return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
    }

    /*
    声明队列,名称为 fanout.queue2
     */
    @Bean
    public Queue fanoutQueue2() {
        return new Queue("fanout.queue2");
    }
    /*
    绑定队列和交换机
     */
    @Bean
    public Binding fanoutBinding2(Queue fanoutQueue2, FanoutExchange fanoutExchange) {
        return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
    }
}

运行完之后,可以在控制台中查看是否自动创建了对应交换机,队列,以及相互绑定。

direct示例

在consumer中创建一个配置类,DirectConfig,声明队列和交换机,direct模式要绑定多个KEY,会非常麻烦,每一个Key都要编写一个binding。

package com.itheima.consumer.config;

import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DirectConfig {
    /*
    声明direct类型交换机
    */
    @Bean
    public DirectExchange directExchange(){
        return new DirectExchange("hmall.direct");
    }

    /*
    声明队列,名称为 direct.queue1
     */
    @Bean
    public Queue directQueue1() {
        return new Queue("direct.queue1");
    }

    /*
    绑定队列和交换机;路由key 为 red
     */
    @Bean
    public Binding directBinding1(Queue directQueue1, DirectExchange directExchange) {
        return BindingBuilder.bind(directQueue1).to(directExchange).with("red");
    }

    /*
    绑定队列和交换机;路由key 为 blue
     */
    @Bean
    public Binding directBinding2(Queue directQueue1, DirectExchange directExchange) {
        return BindingBuilder.bind(directQueue1).to(directExchange).with("blue");
    }

    /*
    声明队列,名称为 direct.queue2
     */
    @Bean
    public Queue directQueue2() {
        return new Queue("direct.queue2");
    }

    /*
    绑定队列和交换机;路由key 为 red
     */
    @Bean
    public Binding directBinding3(Queue directQueue2, DirectExchange directExchange) {
        return BindingBuilder.bind(directQueue2).to(directExchange).with("red");
    }

    /*
    绑定队列和交换机;路由key 为 yellow
     */
    @Bean
    public Binding directBinding4(Queue directQueue2, DirectExchange directExchange) {
        return BindingBuilder.bind(directQueue2).to(directExchange).with("yellow");
    }
}

运行之后,就可以看到自动创建了对应交换机,队列,以及相互绑定

基于注解声明

基于@Bean的方式声明队列和交换机比较麻烦,Spring还提供了基于注解方式来声明,不过是在消息监听的时候基于注解的方式来声明。

列如我们同样声明Direct模式的交换机和队列,修改SpringRabbitListener中对应的listenDirectQueue1和listenDirectQueue2两个方法

	    /*
    监听 direct.queue1 队列的消息
     */
	@RabbitListener(bindings = @QueueBinding(
                value = @Queue("direct.queue1"),
                exchange = @Exchange(value = "hmall.direct", type = ExchangeTypes.DIRECT),
                key = {"red", "blue"}
    ))
    public void listenDirectQueue1(String message) {
        System.out.println("【消费者1】接收到消息: " + message );
    }
    /*
    监听 direct.queue2 队列的消息
     */
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue("direct.queue2"),
            exchange = @Exchange(value = "hmall.direct", type = ExchangeTypes.DIRECT),
            key = {"red", "yellow"}
    ))
    public void listenDirectQueue2(String message) {
        System.out.println("【消费者2】接收到消息: " + message );
    }

消息转换器

Spring的消息发送代码接收的消息体是一个Object:

而在数据传输时,它会把你发送的消息序列化为字节发送给MQ,接收消息的时候,还会把字节发序列化为Java对象

只不过,在默认情况下Spring采用的序列化方式是JDK序列化,但是JDK序列化存在以下问题

  • 数据体积过大
  • 有安全漏洞
  • 可读性差

配置JSON转换器

添加依赖

JDK序列化方式并不合适,我们希望消息体的体积更小,可读性更高,因此可以使用JSON方式来做序列化和反序列化。

publisherconsumer两个服务中都引入依赖

		<dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>

注意:如果项目中已经引入spring-boot-starter-web依赖,则无需再次引入Jackson依赖

配置消息转换器

配置消息转换器,在publisherconsumer两个服务的启动类中添加一个Bean即可

package com.itheima.publisher;

import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class PublisherApplication {
    public static void main(String[] args) {
        SpringApplication.run(PublisherApplication.class);
    }

    @Bean
    public MessageConverter messageConverter() {
        //1、定义消息转换器
        Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter();
        //2、配置每条消息自动创建id;用于识别不同消息,也可以在页面中基于id判断是否是重复消息
        jackson2JsonMessageConverter.setCreateMessageIds(true);
        return jackson2JsonMessageConverter;
    }

}

package com.itheima.consumer;

import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

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

    @Bean
    public MessageConverter messageConverter() {
        //1、定义消息转换器
        Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter();
        //2、配置每条消息自动创建id;用于识别不同消息,也可以在页面中基于id判断是否是重复消息
        jackson2JsonMessageConverter.setCreateMessageIds(true);
        return jackson2JsonMessageConverter;
    }
}

posted @ 2024-02-28 21:11  奕帆卷卷  阅读(48)  评论(0)    收藏  举报