RabbitMQ基础

博文连接🚅🚋🚋🚋🚋🚋🌞

一、MQ基础

1.1、同步调用的优缺点

看到一个简单的业务实现:用户支付后,需要查询订单信息然后调用仓储服务…等一系列服务

image-20231021122832466

  • 耦合度高:每次加入新的需求,都需要修改原来的代码。
  • 性能下降:调用者需要等待服务提供者响应,如果调用链过长则响应时间等于每次调用的时间之和。
  • 资源浪费:调用链中的每个服务在等待响应过程中不能释放请求占用的资源,高并发场景下极度浪费资源。
  • 级联失败:如果服务提供者出现问题,所有调用方都会跟着出现问题。

优点

同步调用虽然有以上的问题,但是相比于异步调用,同步服务的响应更迅速

1.2、异步调用的优缺点

image-20231021123622858

  • 耦合度降低:即增加新服务的时候只需要告知Broker【中间商】添加订阅事件即可
  • 吞吐量提升:同步调用时,需要等待所有服务执行完用户才能得到响应,而异步调用用户向Broker发送请求后,无需等待全部服务的完成即可得到响应结果。
  • 故障隔离:服务之间没有强依赖关系,不担心级联失败问题。
  • 流量削峰:高并发请求通过Broker缓存,微服务基于服务能力从Broker中获取事件,处理事件,起到对微服务的保护作用

缺点:

  1. 依赖于Broker的可靠性、安全性、吞吐能力
  2. 架构复杂,业务没有明显的流程线,不便于追踪管理

1.3、什么是MQ

MQ(MessageQueue)消息队列,也就是异步调用中的Broker

RabbitMQRockerMQKafkaActiveMQ
公司/社区Rabbit阿里ApacheApache
开发语言ErlangJavaScala&JavaJava
协议支持AMQP、XMPP、SMTP、STOMP自定义协议自定义协议OpenWire、STOMP、REST、XMPP、AMQP
可用性一般
单击吞吐量一般非常高
消息延迟微妙级毫秒级毫秒以内毫秒级
消息可靠性一般一般

二、RabbitMQ快速入门

2.1、RabbitMQ部署【下载安装运行】

当前ubuntu系统下使用docker下载镜像文件并运行MQ容器

  1. 运行docker

    systemctl start docker
    
  2. 拉取MQ资源

    docker pull rabbitmq:3-management
    
    image-20231021141825306
  3. 执行以下命令配置并运行MQ容器

    image-20231021142748304

    docker run \
     -e RABBITMQ_DEFAULT_USER=root \			#此处设置用户名
     -e RABBITMQ_DEFAULT_PASS=123456 \			#设置密码
     --name mq \								#创建的容器名字
     --hostname mq1 \							#设置端口名
     -p 15672:15672 \							#设置管理端的端口
     -p 5672:5672 \								#设置用户端的端口
     -d \										#-d参数表示docker后台运行该容器
     rabbitmq:3-management
    
  4. 上述配置成功实现后,我们打开RabbitMQ管理端页面

    image-20231021143641785

2.2、RabbitMQ结构与概念

打开MQ管理页面可以看到如下画面

image-20231021144056228

页面中包含RabbitMQ的几个概念:

  • Channel:操作MQ的工具
  • exchange:路由消息到队列中
  • queue:缓存消息
  • virtual host:虚拟主机,是对queue、exchange等资源的逻辑分组

RabbitMQ的结构

image-20231021144325406

2.3、RabbitMQ消息模型介绍

MQ的官方文档给出了 6 个MQ的Demo示例,下面给出一些常用的用法:

  • 基本消息队列(BasicQueue)
  • 工作消息队列(WorkQueue)
  • 发布订阅(publish Subscribe),根据交换机类型不同分为三种:
    1. Fanout Exchange:广播
    2. Direct Exchange:路由
    3. Topic Exchange:主题

2.4、Helloworld案例

官方的Helloworld是基于最基础的消息队列模型来实现的,只包括三个角色:

image-20231021150126050

  • publisher:消息发布者,将消息发送到队列queue中
  • queue:消息队列,负责接收并缓存消息
  • consumer:订阅队列,处理队列中的信息

打开idea创建两个模块,一个模块用来的当做消息发布者【publisher】,另一个模块用来当做消息接受者【comsumer】

并在这两个模块中添加RabbitMQ的依赖

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

然后在模块消息发布者中书写代码如下:

public class PublisherTest {
    @Test
    public void testSendMessage() throws IOException, TimeoutException {
        // 1.建立连接
        ConnectionFactory factory = new ConnectionFactory();
        // 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
        factory.setHost("192.168.91.134");
        factory.setPort(5672);
        factory.setVirtualHost("/");
        factory.setUsername("root");
        factory.setPassword("123456");
        // 1.2.建立连接
        Connection connection = factory.newConnection();

        // 2.创建通道Channel
        Channel channel = connection.createChannel();

        // 3.创建队列
        String queueName = "simple.queue";
        channel.queueDeclare(queueName, false, false, false, null);

        // 4.发送消息
        String message = "hello, rabbitmq!";
        channel.basicPublish("", queueName, null, message.getBytes());
        System.out.println("发送消息成功:【" + message + "】");

        // 5.关闭通道和连接
        channel.close();
        connection.close();
    }
}

接收模块书写接收消息队列的代码

public class ConsumerTest {

    public static void main(String[] args) throws IOException, TimeoutException {
        // 1.建立连接
        ConnectionFactory factory = new ConnectionFactory();
        // 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
        factory.setHost("192.168.91.134");
        factory.setPort(5672);
        factory.setVirtualHost("/");
        factory.setUsername("root");
        factory.setPassword("123456");
        // 1.2.建立连接
        Connection connection = factory.newConnection();

        // 2.创建通道Channel
        Channel channel = connection.createChannel();

        // 3.创建队列
        String queueName = "simple.queue";
        channel.queueDeclare(queueName, false, false, false, null);

        // 4.订阅消息
        channel.basicConsume(queueName, true, new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope,
                                       AMQP.BasicProperties properties, byte[] body) throws IOException {
                // 5.处理消息
                String message = new String(body);
                System.out.println("接收到消息:【" + message + "】");
            }
        });
        System.out.println("等待接收消息。。。。");
    }
}

根据上述的Helloworld案例,可以知道基本消息队列的消息发送流程:

  1. 建立connection
  2. 创建channel
  3. 利用channel声明队列
  4. 利用channel向队列发送信息

基本消息对立接收流程:

  1. 建立connection
  2. 创建channel
  3. 利用channel声明队列
  4. 定义consumer的消费行为handleDelivery()
  5. 利用channel将消费者与队列绑定

三、SpringAMQP

  • 根据上文Helloworld案例使用官方的API实现的简单队列模型,可以发现使用官方的API操作十分麻烦,因此学习SpringAMQP,可以简化消息发送和接收的API

AMQP:Advanced Message Queuing Protocol ,是用于在应用程序或之间传递业务消息的开放标准。该协议与语言和平台无关,更符合微服务中独立性的要求

SpringAMQP:是基于AMQP协议的一套API规范,提供了模板来发送和接收信息。包含两部分,其中spring-amqp是基础抽象,spring-rabbit是底层的默认实现

3.1、案例:利用Spring-AMQP实现基础消息队列

3.1.1、消息的发送
  1. 在工程中添加spring-amqp的依赖

    <!--AMQP依赖,包含RabbitMQ-->
    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
    
  2. 在发布信息的模块中使用RabbitTemplate发送信息到simple.queue这个队列

    • 为了解除硬编码问题,首先编写配置文件application.yml添加MQ的连接信息

      spring:
        rabbitmq:
          host: 192.168.91.134		# 主机名
          password: 123456			# 密码
          username: root				# 用户名
          port: 5672					# 端口
          virtual-host: /				# 虚拟主机
      
    • 编写一个测试类,测试发送信息到队列simple.queue

      @RunWith(SpringRunner.class)
      @SpringBootTest
      public class SpringAMQPTest {
          @Autowired
          private RabbitTemplate rabbitTemplate;
          @Test
          public void sendMessage(){
              String queueName = "simple.queue";
              String messgae = "这是传递的信息";【
              rabbitTemplate.convertAndSend(queueName,messgae);
          }
      }
      
    • 启动程序并打开RabbitMQ管理端http://192.168.91.134:15672查看信息是否传递到队列

      image-20231021211000025

    • 打开管理端查看队列可以看到确实缓存了一条信息

      image-20231022000737993

注意事项

操作工具类rabbitTemplate发送信息到队列的时候,需要先确保该队列已经存在,否则信息无法发送到队列,并且运行的时候也不会报错。

声明队列的两种方法

  1. 在管理端图形界面手动创建队列

    image-20231021212614024

  2. 使用代码声明队列

    	@Autowired
        private RabbitTemplate rabbitTemplate;
        @Test
        public void sendMessage(){
            String queueName = "sb.queue";
            String messgae = "1231231";
    		                     
            //声明队列
            RabbitAdmin admin = new RabbitAdmin(rabbitTemplate);
            Queue simpleQueue = new Queue(queueName);
            admin.declareQueue(simpleQueue);
            //声明队列后向队列发送信息
            rabbitTemplate.convertAndSend(queueName,messgae);
        }
    

    此时打开管理端可以看到确实创建了一个新队列sb.queue

    image-20231022000128634

其实综上所述,SpringAMQP发送信息无非以下几点

  1. 引入amqp的starter依赖
  2. 配置RabbitMQ的地址
  3. 利用RabbitTemplate的convertAndSend方法
3.1.2、消息的接收
  1. 在工程中添加spring-amqp的依赖【与上文发送消息步骤一致】

    <!--AMQP依赖,包含RabbitMQ-->
    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
    
  2. 创建application.yml配置RabbitMQ的地址【与上文发送信息的步骤一致】

    spring:
      rabbitmq:
        host: 192.168.91.134		# 主机名
        password: 123456			# 密码
        username: root				# 用户名
        port: 5672					# 端口
        virtual-host: /				# 虚拟主机
    
  3. 在接收信息的服务中新建一个类,编写处理信息的逻辑

    @Component			//将该类声明为一个bean让spring能够发现
    public class Listener {
        @RabbitListener(queues = "simple.queue")		//该注解用来声明监听队列的名称
        public void workQueue(String msg) throws InterruptedException {
            System.out.println("spring消费者接收到信息: " + msg + "时间: " +LocalTime.now());
        }
    }
    
  4. 运行并查看成功接收到- 上文发送的消息

    image-20231022003803774

  5. 打开管理端查看队列,可以看到此时队列的信息被取走消息不存在了

    image-20231022003945582

综上所述,SpringAMQP接收消息为以下步骤:

  • 引入amqp的starter依赖
  • 配置RabbitMQ地址
  • 定义类,添加@Component注解,交给Spring管理
  • 类中定义方法,添加@RabbitListener注解,方法参数就是接收的信息类型

3.2、WorkQueue模型

基础消息队列:一个生产者对应一个消费者

工作队列:一个生产者对应多个消费者,多个消费者绑定到同一个队列,同一条消息只会被一个消费者处理

  • 提高消息的处理速度,避免队列消息堆积

image-20231022103055621

3.2.1、模拟workqueue
  • 实现一个队列绑定多个消费者

基本思路如下:

  1. 在publisher服务中定义测试方法,每秒产生50条信息,发送到simple.queue
  2. 在consumer服务中定义两个消息监听者,都监听simple.queue队列
  3. 消费者(1)每秒处理50条信息,消费者(2)每秒处理10条信息

案例实现:

  • 首先定义生产者,每秒发送五十条信息

    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class SpringAMQPTest {
        @Autowired
        private RabbitTemplate rabbitTemplate;
        @Test
        public void sendToWorkQueue() throws InterruptedException {
            String queueName = "simple.queue";
            String message = "工作队列测试信息__";
            for (int i = 1; i <= 50; i++){
                rabbitTemplate.convertAndSend(queueName,message+i);
                Thread.sleep(20);
            }
        }
    }
    

    image-20231022104842073

  • 然后定义两个消费者监听相同的队列simple.queue

    @Component
    public class Listener {
        @RabbitListener(queues = "simple.queue")
        public void workQueue_1(String msg) throws InterruptedException {
            System.out.println("消费者(1)接收: 【" + msg + "】时间: " +LocalTime.now());
            Thread.sleep(20);		//此处调节线程睡眠时间的不同,从而实现控制处理性能的高低
        }
    
        @RabbitListener(queues = "simple.queue")
        public void workQueue_2(String msg) throws InterruptedException {
            System.err.println("消费者(2)接收: 【" + msg + "】时间: " +LocalTime.now());
            Thread.sleep(200);		//此处调节线程睡眠时间的不同,从而实现控制处理性能的高低
        }
    }
    

    image-20231022105036440

    • 启动生产者服务,向队列发送五十条信息,然后启动消费者服务可以看到消息平均分配给两个消费者

    image-20231022110111276

平均分配是为什么呢?

  • 因为没有配置RabbitMQ的消息预取机制,因此每个消费者取信息的能力是无限的,因此上述案例中的两个消费者平均取走了消息,即使二者就算存在了处理性能的差异,因为取走信息的数量是相同的,因此最后处理的数据还是等量的。

如果消费者都是平均分配信息处理,那就没有考虑到每个消费者的性能。

  • 我们希望处理性能高的消费者处理多点信息,处理性能低的消费者少处理点信息,那么总的处理时间就可以大大降低了。

因此需要配置消息预取的数量,这样便可以控制处理越快的消费者获取的消息就越多

spring:
  rabbitmq:
    host: 192.168.91.134
    password: 123456
    username: root
    port: 5672
    virtual-host: /
    listener:
      simple:
        prefetch: 1				# 消息预取数量控制

然后我们重启生产者和消费者服务,再次查看处理信息的情况

image-20231022113412980

3.3、发布订阅模型

前面介绍的案例都是只能实现一条消息一个消费者接收并使用

而发布订阅模型与它们的区别就是允许将同一条消息发送给多个消费者。实现方式是加入了Exchange(交换机)

常见的交换机类型包括:

  • Fanout:广播
  • Direct:路由
  • Topic:话题

image-20231022114425190

注意:exchange只负责消息路由,不是存储,路由失败则消息丢失

3.3.1、Fanout_Exchange

Fanout Exchange会将接收到的消息路由到每一个跟其绑定的queue

利用SpringAMQP演示Fanoutexchange的使用

实现思路如下:

  1. 在消费者服务中,利用代码声明队列、交换机、并将二者绑定
  2. 在消费者服务中,编写两个消费者方法,分别监听fanout.queue(1)和fanout.queue(2)
  3. 在生产者中编写测试方法,向itcast.fanout发送信息

**步骤一:**在consumer服务使用bean声明Exchange、Queue、Binding

在Consumer服务常见的一个类,添加@Configuration注解,并声明FanoutExchange、Queue和绑定关系对象Binding,代码如下:

@Configuration
public class FanoutConfig {
    //声明一个Fanout交换机
    @Bean
    public FanoutExchange fanoutExchange(){
        return new FanoutExchange("mystudy.fanout");
    }

    //声明一个队列
    @Bean
    public Queue fanoutQueue_1(){
        return new Queue("fanout.queue");
    }

    //绑定队列和交换机
    @Bean
    public Binding bindingQueue(Queue fanoutQueue_1,FanoutExchange fanoutExchange){
        return BindingBuilder
            .bind(fanoutQueue_1)
            .to(fanoutExchange);
    }
    
    //..相同方式声明第二个队列,并完成绑定
    @Bean
    public Queue fanoutQueue_2(){
        return new Queue("fanout.queue2");
    }
    @Bean
    public Binding bindingQueue2(Queue fanoutQueue_2,FanoutExchange fanoutExchange){
        return BindingBuilder
            .bind(fanoutQueue_1)
            .to(fanoutExchange);
    }
}

启动服务后,打开MQ管理端查看是否成功绑定了交换机与队列的关系

image-20231022144702393

步骤二:队列与交换机绑定关系成功后,在consumer服务中定义监听函数,监听两个队列

步骤三并在publisher中发送信息到交换机

image-20231022145924938

启动服务可以看到两个队列都收到了消息:可知交换机将一条消息发给了绑定关系的两个队列

image-20231022150012898综上所述:交换机的作用是什么?

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

声明队列、交换机、绑定关系的Bean是什么?

  • Queue
  • FanoutExchange
  • Binding
3.3.2、Direct_Exchange

Direct Exchange会将接收到的信息根据规则路由到指定的Queue,因此称为路由模式(routes)。

  • 每一个Queue都与Exchange设置一个BindingKey
  • 发布者发送消息时,指定消息的RoutingKey
  • Exchange将消息路由到BindingKey与消息RoutingKey一致的队列

image-20231022152327701

利用SpringAMQP演示DirectExchange的使用

实现思路如下:

  1. 利用注解@RabbitListener声明Exchange、Queue、RoutingKey【原来的使用bean来配置Exchange、Queue、RoutingKey太复杂,因此直接在注解中声明即可】-点击查看使用bean配置的例子

  2. 在consumer服务中,编写两个消费者方法,分别监听direct.queue1direct.queue2

    @RabbitListener(bindings = @QueueBinding(
    	value = @Queue(name="direct.queue1"),
    	exchange = @Exchange(name = "mystudy.direct",type = ExchangeTypes.DIRECT),
    	key = {"red","blue"}
    ))
    public void listenDirect_1(String msg){
    	System.out.println("接收到direct.queue_1的信息:【" + msg + "】");
    }
    
    @RabbitListener(bindings = @QueueBinding(
    	value = @Queue(name="direct.queue2"),
    	exchange = @Exchange(name = "mystudy.direct",type = ExchangeTypes.DIRECT),
    	key = {"red","yellow"}
    ))
    public void listenDirect_2(String msg){
    	System.out.println("接收到direct.queue_2的信息:【" + msg + "】");
    }
    

    image-20231022155509942

    启动服务查看交换机的配置详情

    image-20231022155944510

  3. 在publisher中编写测试方法,向mystudy.direct发送信息,启动服务观察结果

    image-20231022160845453

    因此可以验证:Direct_Exchange将消息路由到BindingKey与消息RoutingKey一致的队列

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

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

基于注解@RabbitListener注解声明队列和交换机有哪些常见注解

  • @Queue
  • @Exchange
3.3.3、Topic_Exchange

TopicExchange和DirectExchange类似,区别在于routintKey必须是多个单词的列表,并且以.分割

Queue与Exchange指定BindingKey时可以使用通配符:

#:指代0个或多个单词

*:指代一个单词

image-20231022164636256

利用SpringAMQP演示TopicExchange的使用

实现思路如下:

  1. 利用@RabbitListener声明Exchange、Queue、RoutingKey

  2. 在Consumer服务中,编写两个消费者方法,分别监听topic.queue1topic.queue2

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name="topic.queue1"),
            exchange = @Exchange(name = "mystudy.topic",type = ExchangeTypes.TOPIC),
            key = "china.#"
    ))
    public void listenTopic_1(String msg){
        System.out.println("接收到topic.queue_1的信息:【" + msg + "】");
    }
    
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name="topic.queue2"),
            exchange = @Exchange(name = "mystudy.topic",type = ExchangeTypes.TOPIC),
            key = "#.news"
    ))
    public void listenTopic_2(String msg){
        System.out.println("接收到topic.queue_2的信息:【" + msg + "】");
    }
    
  3. 在publisher中编写测试方法,向mystudy.topic发送信息

    @Test
    public void SendTo_China_News(){
    	String exchangeName = "mystudy.topic";
    	String msg = "交换机topic测试信息__{China and News}__";
    	String routingKey = "china.news";
    	rabbitTemplate.convertAndSend(exchangeName,routingKey,msg);
    }
    @Test
    public void SendTo_News(){
    	String exchangeName = "mystudy.topic";
    	String msg = "交换机topic测试信息__{China and News}__";
    	String routingKey = "test.news";
        rabbitTemplate.convertAndSend(exchangeName,routingKey,msg);
    }
    

3.4、消息转换器

说明:在SpringAMQP的发送方法中,接收信息的类型是Object,也就是说我们可以发送任意对象类型的消息,SpringAMQP会帮我们序列化为字节后发送。

验证:查看RabbitTempate的convertAndSend函数接收的参数

image-20231022170400657

查看原来的消息转换器接收对象之后的处理结果,然后在使用新的转换器

第一步:定义一个新的队列用来接收对象

@Bean
public Queue objectQueue(){
	return new Queue("object.queue");
}

第二步:编写发送信息的函数

@Test
public void SendToObjQueue(){
	Map<String, Object> msg = new HashMap<>();
	msg.put("name","张三");
	msg.put("age",21);
	rabbitTemplate.convertAndSend("object.queue",msg);
}

第三步:打开管理端查看存储的内容可以看到存储的内容是Java序列化的对象

image-20231022185832721

Spring的对消息的处理是由org.springframework.amqp.support.converter.MessageConverter来处理的。而默认实现是SimpleMessageConverter,基于JDK的ObjectOupytStream完成序列化。

如果要修改只需要定义一个MessageConverter类型的bean即可。推荐使用JSON方式序列化,步骤如下:

  • 首先在publisher服务中引入依赖

    <dependency>
        <groupId>com.fasterxml.jackson.dataformat</groupId>
        <artifactId>jackson-dataformat-xml</artifactId>
        <version>2.13.3</version>
    </dependency>
    
  • 在publisher服务声明MessageConverter

    @Bean
    public MessageConverter messageConverter(){
    	return new Jackson2JsonMessageConverter();
    }
    
  • 上述配置完成后重新发送对象信息,再次打开管理端查看存储的内容:

    image-20231022194007063

  • 上述就已经完成了对发送消息时候的格式转换,接收消息的时候同样需要进行消息的转换,因此引入依赖,配置转换器两部的操作是一致的。

  • 首先在consumer服务中引入依赖

    <dependency>
        <groupId>com.fasterxml.jackson.dataformat</groupId>
        <artifactId>jackson-dataformat-xml</artifactId>
        <version>2.13.3</version>
    </dependency>
    
  • 在consumer服务声明MessageConverter

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

综上所述:SpringAMQP中消息的序列化和反序列化是如何实现的?

  • 利用MessageConverter实现的,默认是JDK的序列化
  • 注意发送方与接收方必须使用相同MessageConverter
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

橙子哥_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值