一篇文章你了解不了RabbitMQ
前言
本文意在让大家了解RabbitMQ, 本来想写多一些, 但是感觉一篇文章写不下, 所以这篇文章就带大家了解RabbitMQ的作用, 和几种工作模式, 及SpringBoot整合RabbitMQ的示例;
rabbitmq介绍
rabbitmq是一种用于上下游传递消息的分布式消息中间件, 由于其可靠性, 并发性, 可用性 在众多中间件中表现出色所以深受互联网公司喜爱;
rabbitmq官网:https://www.rabbitmq.com/getstarted.html
说到rabbitmq的作用其实就是所有消息中间件的作用, 说白了就是引入消息中间件来解决什么:
- 异步
- 解耦
- 削峰
RabbitMQ 提供了 5 种工作模式:
- 简单模式
- work 模式
- Publish/Subscribe 发布与订阅模式
- Routing 路由模式
- Topics 主题模式
那么我们来看看这, 三种作用, 五种工作模式 都是什么高考题
异步处理
场景说明:用户注册后,需要发注册邮件和注册短信, 传统的做法有两种 1.串行的方式 2.并行的方式
- 串行方式: 将注册信息写入数据库后, 发送注册邮件, 再发送注册短信, 以上三个任务全部完成后才返回给客户端。
- 并行方式: 将注册信息写入数据库后, 发送邮件的同时发送短信以上三个任务完成后, 返回给客户端。
上面两种方式有一个问题邮件, 短信他们成功响应对于系统来说并不是必须的, 它们只是一个通知, 而这种做法会让客户端等待没有必要等待的东西; 下面来看看MQ如何解决该问题
- 消息队列(异步处理)
引入消息队列后, 把发送邮件, 短信不是必须返回结果的业务逻辑异步处理, 由此可以看出, 引入消息队列后,用户的 响应时间 = 写入数据库的时间 + 写入消息队列的时间;
应用解耦
场景说明:双11是购物狂节, 用户下单后, 订单系统需要通知库存系统, 传统的做法就是订单系统调用库存系统的接口, 库存处理成功时返回结果, 这时订单系统收到库存的响应, 用户才会下单成功;
这种做法有一个缺点: 当库存系统出现故障时, 订单就会失败, 订单系统和库存系统高耦合;
引入消息队列解耦应用:
订单系统: 用户下单后, 订单系统完成持久化处理, 将消息写入消息队列, 返回用户订单下单成功。
库存系统: 订阅下单的消息, 获取下单消息, 进行库操作。 就算库存系统出现故障, 消息队列也能保证消息的可靠投递, 不会导致消息丢失。
削峰填谷
场景说明:秒杀活动, 一般会因为流量过大, 导致应用挂掉, 而活动结束后流量又恢复稳定, 传统的做法就是购买临时服务器, 增加抗压性, 还有就是在应用前端加入消息队列。
作用:
- 控制活动人数,超过此一定阀值的订单直接丢弃(适用于秒杀);
- 缓解短时间的高流量压垮应用, 流量高峰时可以将用户下单信息存储到队列中, 流量低时慢慢处理;
介绍工作模式前, 我们先安装Rabbitmq 并集成到Springboot项目中;
安装Rabbitmq
rabbitmq是由Erlang语言编写的, 所以想运行, 必须要安装Erlang环境;
Erlang安装步骤:
- 下载安装包
- 以管理员方式运行
- 安装位置选择默认(记下安装地址)
- 将Erlang添加到Path环境变量中
安装了erlang环境后, 我们在来安装rabbitmq, 以管理员运行安装程序, 没有其他操作 正常安装即可;
默认情况安装成功后会自动创建RabbitMQ服务并且启动,所以无需任何操作,但是为了方便在浏览器端管理RabbitMQ,需要安装管理软件,进入到安装目录的sbin目录下使用命令行程序:
运行:
rabbitmq-plugins.bat enable rabbitmq_management
如图所示就是安装成功了;
浏览器输入:http://localhost:15672/
初始账号密码:guest/guest
到此我们rabbitmq就安装成功了, 下面我们用SpringBoot 集成Rabbitmq, 并测试五种工作模式;
SpringBoot 集成Rabbitmq
我们需要创建2个springboot项目,一个 rabbitmq-provider (生产者),一个rabbitmq-consumer(消费者);
生产者
使用Maven创建名为rabbitmq-provider 空服务
Maven依赖:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
在配置文件application.yml中添加rabbitmq的连接信息
server:
port: 8081
spring:
rabbitmq:
#ip地址
host: 127.0.0.1
#通信端口
port: 5672
username: guest #用户名
password: guest #密码
virtual-host: / #虚拟host
初始化队列信息
package com.example.rabbitmqprovider.config;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitMQConfig {
/**
* 创建hello_world队列
*
* @param
* @return
* @author Hope
*/
@Bean
public Queue hello() {
return new Queue("hello_world");
}
}
使用RabbitTemplate
发送消息
package com.example.controller;
import com.example.model.User;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/sendMsg")
public void sendMsg(){
rabbitTemplate.convertAndSend("hello_world", "hello 小兔子");
}
}
最终效果如下:
消费者
使用Maven创建名为rabbitmq-consumer 空服务
Maven依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.example</groupId>
<artifactId>rabbitmq-consumer</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
在配置文件application.yml中添加rabbitmq的连接信息
server:
port: 8082
spring:
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
virtual-host: /
监听信息
@Component
@RabbitListener(queues = "hello_world") //监听hello_world队列
public class ConsumerService {
/*
监听字符串消息
*/
@RabbitHandler
public void helloConsumer(String message){
System.out.println("消费者信息: " + message);
}
}
最终效果如下:
然后我们启动生产者服务, 访问接口, 观察rabbitmq界面, 这时队列里面出现hello_world队列, 并且队列已经就绪一条消息。
然后我们在启动消费者, 消费者打印出生产者的消息, 我们这时在查看rabbitmq界面, 可以看到生产者的消息已经被消费
以上就是rabbitmq的简单工作模式
小节:
在上图的模型中,有以下概念:
- P:生产者,也就是要发送消息的程序
- C:消费者:消息的接收者,会一直等待消息到来
- queue:消息队列,图中红色部分。类似一个邮箱,可以缓存消息;生产者向其中投递消息,消费者从其中取出消息
- @RabbitListener 用于指定监听的队列名称
- @RabbitHandler 用于区分同队列不同消息体的处理
工作队列模式
Work Queues(工作队列):与入门程序的简单模式相比,多了一个或一些消费端,多个消费端共同消费同一个队列中的消息。
应用场景:对于任务过重或任务较多情况使用工作队列可以提高任务处理的速度。
具体实现: Work Queues 与入门程序的简单模式的代码几乎是一样的。可以完全复制,并多复制一个消费者进行多个消费者同时对消费消息的测试。
复制rabbitmq-consumer为rabbitmq-consumer2,并修改打印信息用于区分;
项目效果如下:
然后我们启动 生产者 消费者1 消费者2, 然后我们让生产者生成10条消息, 最终效果如下:
Work Queues 对于任务过重或任务较多情况使用工作队列可以提高任务处理的速度。例如:短信服务部署多个,只需要有一个节点成功发送即可。
但是有一点需要注意该工作队列模式使用的是轮询模式, 这种工作队列太过于理想化,并不适合实际的开发,我们难以保证每个消费者消费信息的速度相同,难免有的快有的慢;
发布与订阅模式
在订阅模型中,多了一个 Exchange 角色,而且过程略有变化:
- P:生产者,也就是要发送消息的程序,但是不再发送到队列中,而是发给X(交换机)
- C:消费者,消息的接收者,会一直等待消息到来
- Queue:消息队列,接收消息、缓存消息
- Exchange:交换机(X)。一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。Exchange有常见以下3种类型:
- Fanout:广播,将消息交给所有绑定到交换机的队列
- Direct:定向,把消息交给符合指定routing key 的队列
- Topic:通配符 ,把消息交给符合routing pattern(路由模式) 的队列
Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与 Exchange 绑定,或者没有符合路由规则的队列,那么消息会丢失!
Fanout广播模式
创建Maven空项目 fanout-rabbitmq子工程,
Maven依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.example</groupId>
<artifactId>fanout-rabbitmq</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.example</groupId>
<artifactId>fanout-rabbitmq</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
在配置文件中添加rabbitmq的连接信息
server:
port: 8084
spring:
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
virtual-host: /
初始化队列、交换机信息
package com.example.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 FanoutRabbitMQConfig {
/*
创建fanout.A队列
*/
@Bean
public Queue fanoutAQueue(){
return new Queue("fanout.A");
}
/*
创建fanout.B队列
*/
@Bean
public Queue fanoutBQueue(){
return new Queue("fanout.B");
}
/*
创建fanout.C队列
*/
@Bean
public Queue fanoutCQueue(){
return new Queue("fanout.C");
}
/*
创建交换机
*/
@Bean
public FanoutExchange fanoutExchange(){
return new FanoutExchange("my_fanoutExchange");
}
/*
将fanout.A绑定到 fanoutExchange交换机
*/
@Bean
Binding bindingExhangeA(Queue fanoutAQueue, FanoutExchange fanoutExchange){
return BindingBuilder.bind(fanoutAQueue).to(fanoutExchange);
}
/*
将fanout.B绑定到 fanoutExchange交换机
*/
@Bean
Binding bindingExhangeB(Queue fanoutBQueue, FanoutExchange fanoutExchange){
return BindingBuilder.bind(fanoutBQueue).to(fanoutExchange);
}
/*
将fanout.C绑定到 fanoutExchange交换机
*/
@Bean
Binding bindingExhangeC(Queue fanoutCQueue, FanoutExchange fanoutExchange){
return BindingBuilder.bind(fanoutCQueue).to(fanoutExchange);
}
}
使用RabbitTemplate
发送消息
package com.example.controller;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class FanoutController {
@Autowired
private RabbitTemplate rabbitTemplate;
/*
fanout模式发送简单字符串信息
*/
@GetMapping("/sendMsg")
public void sendMsg() {
for (int i = 0; i <= 30; i++) {
rabbitTemplate
.convertAndSend("my_fanoutExchange", "", "hello 小兔子: " + i);
}
}
}
监听信息
这里使用之前的rabbitmq-consumer 服务来监听fanout.A队列
修改rabbitmq-consumer代码如下
这里我们继续使用之前的rabbitmq-consumer2 服务来监听fanout.B队列, 修改如上一样
@Component
@RabbitListener(queues = "hello_world") //监听hello_world队列
@RabbitListener(queues = "fanout.B")
public class ConsumerService {
/*
监听字符串消息
*/
@RabbitHandler
public void helloConsumer(String message) throws InterruptedException {
Thread.sleep(3000);
System.out.println("消费者2信息: " + message);
}
}
然后我们启动 fanout-rabbitmq 消费者1 消费者2, 然后我们让fanout生成30条消息, 最终效果如下:
小结:
交换机需要与队列进行绑定,绑定之后;一个消息可以被多个消费者都收到,实质是一个广播消息。
发布订阅模式与工作队列模式的区别:
- 工作队列模式不用定义交换机,而发布/订阅模式需要定义交换机,工作队列模式底层使用的是默认的交换机
- 发布/订阅模式需要设置队列和交换机的绑定,工作队列模式不需要设置,实际上工作队列模式会将队列绑定到默认的交换机
Routing路由模式
- 队列与交换机的绑定,不能是任意绑定了,而是要指定一个 RoutingKey(路由key)
- 消息的发送方在向 Exchange 发送消息时,也必须指定消息的 RoutingKey
- Exchange 不再把消息交给每一个绑定的队列,而是根据消息的 Routing Key 进行判断,只有队列的Routingkey 与消息的 Routing key 完全一致,才会接收到消息
图解:
- P:生产者,向 Exchange 发送消息,发送消息时,会指定一个routing key
- X:Exchange(交换机),接收生产者的消息,然后把消息递交给与 routing key 完全匹配的队列
- C1:消费者,其所在队列指定了需要 routing key 为 error 的消息
- C2:消费者,其所在队列指定了需要 routing key 为 info、warning 的消息
创建Maven空项目
routing-rabbitmq子工程,
Maven依赖, spring配置都和广播的一样, 这里就不贴了;
初始化队列、交换机
package com.example.config;
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;
@Configuration
public class RoutingRabbitMQConfig {
@Bean
public Queue directAQueue() {
return new Queue("direct.A");
}
@Bean
public Queue directBQueue() {
return new Queue("direct.B");
}
@Bean
public DirectExchange directExchange() {
return new DirectExchange("my_directExchange");
}
@Bean
Binding bindingAQueue(Queue directAQueue, DirectExchange directExchange) {
return BindingBuilder.bind(directAQueue).to(directExchange).with("error");
}
@Bean
Binding bindingBQueueInfo(Queue directBQueue, DirectExchange directExchange) {
return BindingBuilder.bind(directBQueue).to(directExchange).with("info");
}
@Bean
Binding bindingBQueueWaring(Queue directBQueue, DirectExchange directExchange) {
return BindingBuilder.bind(directBQueue).to(directExchange).with("waring");
}
}
使用RabbitTemplate
发送消息
@RestController
public class RoutingController {
@Autowired
private RabbitTemplate rabbitTemplate;
/*
error信息 只有direct.A 可以接收并消费
*/
@GetMapping("/sendErrorMsg")
public void sendMsg() {
rabbitTemplate.convertAndSend("my_directExchange", "error", "error 小兔子");
}
/*
info 信息 只有direct.B 可以接收并消费
*/
@GetMapping("/sendInfoMsg")
public void sendUser() {
rabbitTemplate.convertAndSend("my_directExchange", "info", "info 小兔子");
}
}
消费者监听信息
这里我们继续使用之前的消费者来监听消息, rabbitmq-consumer和rabbitmq-consumer2;
修改如下:
小结:
Routing 模式要求队列在绑定交换机时要指定 routing key,消息会转发到符合 routing key 的队列。
Topic主题模式
- Topic 类型与 Direct 相比,都是可以根据 RoutingKey 把消息路由到不同的队列。只不过 Topic 类型Exchange 可以让队列在绑定 Routing key 的时候使用通配符!
- Routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert
- 通配符规则:# 匹配大于等于零个,* 匹配不多不少恰好1个词
- 例如:item.# 能够匹配 item.insert.abc 或者 item.insert,item.* 只能匹配 item.insert
创建Maven空项目
topic-rabbitmq 子工程, 具体操作不在演示了, 上面都有;
初始化队列、交换机
/**
* explain:RabbitMQConfig
*
* @author Hope
* @date 2022/6/14
* @see TopicRabbitMQConfig
*/
@Configuration
public class TopicRabbitMQConfig {
@Bean
public Queue topicAQueue() {
return new Queue("topic.A");
}
@Bean
public Queue topicBQueue() {
return new Queue("topic.B");
}
@Bean
public TopicExchange topicExchange() {
return new TopicExchange("my_topicExchange");
}
/*
* 匹配一个词
*/
@Bean
Binding bindingAQueue(Queue topicAQueue, TopicExchange topicExchange) {
return BindingBuilder.bind(topicAQueue).to(topicExchange).with("tikie.*");
}
/*
# 匹配≥零个词
*/
@Bean
Binding bindingBQueue(Queue topicBQueue, TopicExchange topicExchange) {
return BindingBuilder.bind(topicBQueue).to(topicExchange).with("tikie.#");
}
}
使用RabbitTemplate
发送消息
package com.example.controller;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TopicController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/sendTopic")
public void sendTopic(){
/*
routingkey三种情况:
1.tikie: 只有topic.B可以接收到消息
2.tikie.heihei: topic.A 和 topic.B都可以接收到消息
3.tikie.heihei.hehe 只有topic.B可以接收到消息
*/
rabbitTemplate.convertAndSend("my_topicExchange", "tikie.heihei.hehe", "hello 小兔子");
}
}
消费者监听信息
这里我们继续使用之前的消费者来监听消息, rabbitmq-consumer和rabbitmq-consumer2;
rabbitmq-consumer2代码
@Component
@RabbitListener(queues = "hello_world") //监听hello_world队列
@RabbitListener(queues = "fanout.B")
@RabbitListener(queues = "direct.B")
@RabbitListener(queues = "topic.B")
public class ConsumerService {
/*
监听字符串消息
*/
@RabbitHandler
public void helloConsumer(String message) throws InterruptedException {
Thread.sleep(3000);
System.out.println("消费者2信息: " + message);
}
}
小结
- Topic 主题模式可以实现 Pub/Sub 发布与订阅模式和 Routing 路由模式的功能,只是 Topic 在配置routing key 的时候可以使用通配符,显得更加灵活。
工作模式总结
-
简单模式 HelloWorld:
- 一个生产者、一个消费者,不需要设置交换机(使用默认的交换机)。
-
工作队列模式 Work Queue
- 一个生产者、多个消费者(轮询),不需要设置交换机(使用默认的交换机)。
-
发布订阅模式 Publish/subscribe(fanout广播模式)
- 需要设置类型为 fanout 的交换机,并且交换机和队列进行绑定,当发送消息到交换机后,交换机会将消息发送到绑定的队列。
-
路由模式 Routing(direct模式)
- 需要设置类型为 direct 的交换机,交换机和队列进行绑定,并且指定 routing key,当发送消息到交换机后,交换机会根据 routing key 将消息发送到对应的队列。
-
通配符模式 Topic
- 需要设置类型为 topic 的交换机,交换机和队列进行绑定,并且指定通配符方式的 routing key,当发送消息到交换机后,交换机会根据 routing key 将消息发送到对应的队列。