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;
}
}

本文作者:奕帆卷卷

本文链接:https://www.cnblogs.com/yifan0820/p/18041860

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   奕帆卷卷  阅读(19)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起