文章目录
认识RabbitMQ
RabbitMQ的架构如图

重要概念:
publisher:生产者,也就是发送消息的一方consumer:消费者,也就是消费消息的一方queue:队列,存储消息。生产者投递的消息会暂存在消息队列中,等待消费者处理exchange:交换机,负责消息路由。生产者发送的消息由交换机决定投递到哪个队列。virtual host:虚拟主机,起到数据隔离的作用。每个虚拟主机相互独立,有各自的exchange、queue
1. 安装
我们使用docker来安装RabbitMQ,如果对docker还不熟悉,可以先看这篇文章:
https://blog.youkuaiyun.com/fim77/article/details/143844100(推荐快速入门,后续学习其他知识也会方便很多)
1.1 拉取镜像
docker pull rabbitmq:management

1.2 运行容器
docker run -d --hostname my-rabbit --name rabbit-mq -p 5672:5672 -p 15672:15672 -e RABBITMQ_DEFAULT_USER=paran -e RABBITMQ_DEFAULT_PASS=123 rabbitmq:management

1.3 访问RabbitMQ管理端页面
输入上一步创建容器时设置的用户名和密码,登录(如果没设置,默认账号和密码是guest)

2. 收发消息
2.1 交换机(exchange)
tip:交换机只转发消息,不会保存消息
RabbitMQ默认会初始化一些交换机,交换机(Exchange)是消息队列系统中的一个核心概念,它决定了消息将被发送到哪个或哪些队列。当生产者(Producer)发送消息时,并不是直接将消息发送到队列中,而是先发送给交换机。交换机会根据特定的规则和绑定(Bindings)决定如何处理这些消息,比如是否丢弃、路由到一个或多个队列等。

交换机的类型有四种:
Fanout:广播,将消息交给所有绑定到交换机的队列。Direct:订阅,基于RoutingKey(路由key)发送给订阅了消息的队列Topic:通配符订阅,与Direct类似,只不过RoutingKey可以使用通配符Headers:头匹配,基于MQ的消息头匹配,用的较少。
1) Fanout 交换机
特点:Fanout 交换机会将接收到的消息广播到所有绑定到该交换机的队列。
工作原理:当消息到达 fanout 交换机时,它会直接将该消息复制并发送到所有绑定在这个交换机上的队列,而不管消息的内容、路由键(routing key)是什么。
用途:适用于广播模式,比如广播通知给多个消费者,所有队列都会收到消息。
2) Direct 交换机
特点:Direct 交换机基于精确匹配的路由键将消息发送到队列。
工作原理:当消息到达 direct 交换机时,交换机会根据消息的路由键(routing key)找到与之完全匹配的队列,并将消息传递给该队列。
用途:适用于一对一的路由模式,比如基于某些条件决定将消息发送到特定队列的场景。
示例:你有一个 direct 交换机,并且有多个队列,每个队列绑定一个特定的路由键。比如队列A绑定的路由键是"error",队列B绑定的路由键是"info"。如果你发送一个路由键为"error"的消息,只有队列A会收到该消息。
3) Topic 交换机
特点:Topic 交换机允许更复杂的路由规则,基于路由键的模式匹配来决定消息的传递。
工作原理:Topic 交换机会根据消息的路由键和队列绑定时指定的模式进行匹配。路由键通常由多个单词组成,用点(.)分隔。可以使用通配符(*和#)来匹配路由键中的部分内容。
*(星号):匹配一个词。
#(井号):匹配零个或多个词。
用途:适用于更复杂的消息路由需求,比如基于多个条件选择性地发送消息。
示例:
你有一个 topic 交换机和多个队列,每个队列都绑定了不同的模式。比如:
队列A绑定模式"*.error",这意味着它将接收所有路由键包含error的消息(例如"system.error")。
队列B绑定模式"*.info",它将接收所有路由键包含info的消息(例如"system.info")。
队列C绑定模式"system.#",它将接收所有以"system."开头的消息。
当发送一个路由键为"system.error"的消息时,队列A和队列C都会接收到这条消息。
2.2 队列
基本上不需要改什么设置,填个队列名字就可以创建队列了

创建完之后就可以看到队列的信息总览了

2.3 绑定关系
这里我们用fanout类型的交换机作为演示,绑定交换机到hello.q1队列

绑定完之后能查看绑定关系


2.4 发消息
在交换机这里可以模拟发消息

在队列这里可以看到消息已经发过来了,因为这里是管理端,能看到消息,并不是把消息消费了,看了之后消息仍然在队列里面

3. 数据隔离
假设有两个用户,他们的交换机、队列等数据需要相互隔离,互不影响,这里创建一个新账户做演示

可以看到新账户是没有virtual hosts的,对于默认账号,是有一个名字为/的virtual hosts的

切换刚创建的账号登录,添加一个virtual hosts

创建好之后可以看到新创建的virtual hosts已经归属到当前账号了

但是我们仍然能看到其他虚拟主机的信息

可以在右上角切换虚拟主机,可以只查看指定虚拟主机的内容

4. SpringAMQP
将来开发编程肯定不是在控制台这样手动操作的,是需要写在代码里面的,手动操作繁琐,容易出错,而且也不方便自动化执行,如果从研发环境转到生产环境,这些配置肯定不可能再手动点一遍配出来,如果能通过执行代码的方式把这些交换机、队列重新自动化的配置好,那肯定是更准确,更方便的。
由于RabbitMQ采用了AMQP协议,因此它具备跨语言的特性。任何语言只要遵循AMQP协议收发消息,都可以与RabbitMQ交互。并且RabbitMQ官方也提供了各种不同语言的客户端。
但是,RabbitMQ官方提供的Java客户端编码相对复杂,一般生产环境下我们更多会结合Spring来使用。而Spring的官方刚好基于RabbitMQ提供了这样一套消息收发的模板工具:SpringAMQP。并且还基于SpringBoot对其实现了自动装配,使用起来非常方便。
4.1 环境配置
创建一个父工程,俩子工程,父工程只有一个pom文件,只用来做依赖管理
子工程一个用来发消息,一个用来接消息
父pom如下
<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>
<groupId>com.paran.test</groupId>
<artifactId>parent-project</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.12</version> <!-- 版本根据你的需求进行修改 -->
</parent>
<name>parent-project</name>
<url>http://maven.apache.org</url>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.9.10</version>
</dependency>
</dependencies>
<modules>
<module>consumer</module>
<module>publisher</module>
</modules>
</project>
子工程继承父工程,pom如下,另外一个类似
<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>com.paran.test</groupId>
<artifactId>parent-project</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath> <!-- 指向父POM的相对路径 -->
</parent>
<artifactId>consumer</artifactId>
<packaging>jar</packaging>
<name>consumer</name>
<url>http://maven.apache.org</url>
</project>
4.2 消息收发初体验
1) 消息发送
hello.q1是之前演示的时候创建的队列,可以测试一下往这个队列发消息
publisher模块(子工程之一)的配置文件:
spring:
rabbitmq:
host: localhost # 你的虚拟机IP
port: 5672 # 端口
virtual-host: / # 虚拟主机
username: paran # 用户名
password: 123 # 密码
测试代码
package com.paran.test;
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 SpringAmpqTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testPublish() {
String queueName = "hello.q1";
String message = "Hello World!";
rabbitTemplate.convertAndSend(queueName, message);
}
}
发完之后在RabbitMQ的后台可以看到消息的内容

2)消息接收
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class Listener {
@RabbitListener(queues = "hello.q1")
public void listenQueueMessage(String msg){
System.out.println("spring 消费者接收到消息:【" + msg + "】");
}
}
2025-04-19 15:17:58.762 INFO 5780 --- [ main] com.paran.App : Starting App using Java 1.8.0_151 on c42038b38c1bfcd with PID 5780 (E:\projects\java_projects\RabbitMQ\consumer\target\classes started by Paran in E:\projects\java_projects\RabbitMQ)
2025-04-19 15:17:58.763 INFO 5780 --- [ main] com.paran.App : No active profile set, falling back to 1 default profile: "default"
2025-04-19 15:17:59.546 INFO 5780 --- [ main] o.s.a.r.c.CachingConnectionFactory : Attempting to connect to: [localhost:5672]
2025-04-19 15:17:59.562 INFO 5780 --- [ main] o.s.a.r.c.CachingConnectionFactory : Created new connection: rabbitConnectionFactory#3e8f7922:0/SimpleConnection@4b4dd216 [delegate=amqp://paran@127.0.0.1:5672/, localPort= 58179]
spring 消费者接收到消息:【hello,778899】
spring 消费者接收到消息:【Hello World!】
2025-04-19 15:17:59.588 INFO 5780 --- [ main] com.paran.App
4.3 声明队列和交换机
之前的队列和交换机,以及他们的绑定关系都是在控制台操作的,但是在实际开发时,队列和交换机是程序员定义的,将来项目上线,又要交给运维去创建。那么程序员就需要把程序中运行的所有队列和交换机都写下来,交给运维。在这个过程中是很容易出现错误的。因此推荐的做法是由程序启动时检查队列和交换机是否存在,如果不存在自动创建。
1) 基于@Bean注解
@Configuration
public class FanoutConfig {
@Bean
public FanoutExchange fanoutExchange(){
//return ExchangeBuilder.fanoutExchange("new.fanout").build();
return new FanoutExchange("new.fanout");
}
@Bean
public Queue fanoutQueue1(){
//QueueBuilder.durable("fanout.queue1").build();
return new Queue("fanout.queue1");
}
@Bean
public Binding bindingQueue1(Queue queue,FanoutExchange exchange){
return BindingBuilder.bind(queue).to(exchange);
}
}
项目启动起来自动创建交换机和队列,项目关闭之后交换机和队列仍然会保留


演示一下direct类型的交换机如何绑定队列
public class DirectConfig {
@Bean
public DirectExchange directExchange(){
return ExchangeBuilder.directExchange("new.direct").build();
}
@Bean
public Queue directQueue1(){
return new Queue("direct.queue1");
}
@Bean
public Binding bindingQueue1WithRed(Queue directQueue1, DirectExchange directExchange){
return BindingBuilder.bind(directQueue1).to(directExchange).with("red");
}
@Bean
public Binding bindingQueue1WithBlue(Queue directQueue1, DirectExchange directExchange) {
return BindingBuilder.bind(directQueue1).to(directExchange).with("blue");
}
}
这里使用.with()来绑定key,每个队列都要绑定一次,甚至一个队列要绑定多个key,这样做太麻烦了,下面介绍另外一种办法
2) 基于@RabbitListener注解(推荐)
@Component
public class DirectConfig {
@RabbitListener(bindings = @QueueBinding(value = @Queue(name = "direct.queue1"),
exchange = @Exchange(name = "new.direct",type = ExchangeTypes.DIRECT),key = {"red","blue"}))
public void listenDirectQueue1(String msg){
System.out.println("消费者1接收到direct.queue1的消息:【" + msg + "】");
}
}
这样也会自动创建交换机和队列,在单元测试中可以发个消息试一下能不能收到
如果key不是指定的red或者blue是收不到消息的
rabbitTemplate.convertAndSend一般有两个常用的重载
两个参数的:参数一是队列名,参数二是消息
三个参数的:参数一是交换机名,参数二是路由key,参数三是消息
参数二可以写""或者null
@SpringBootTest
public class AppTest
{
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSimpleQueue() {
String exchangeName= "new.direct";
String message = "Hello 1234567";
// 发送消息
//rabbitTemplate.convertAndSend(queueName, message);
rabbitTemplate.convertAndSend(exchangeName,"blue",message);
}
}
2025-04-19 16:27:04.064 INFO 11324 --- [ main] com.paran.App : Starting App using Java 1.8.0_151 on c42038b38c1bfcd with PID 11324 (E:\projects\java_projects\RabbitMQ\consumer\target\classes started by Paran in E:\projects\java_projects\RabbitMQ)
2025-04-19 16:27:04.065 INFO 11324 --- [ main] com.paran.App : No active profile set, falling back to 1 default profile: "default"
2025-04-19 16:27:04.829 INFO 11324 --- [ main] o.s.a.r.c.CachingConnectionFactory : Attempting to connect to: [localhost:5672]
2025-04-19 16:27:04.844 INFO 11324 --- [ main] o.s.a.r.c.CachingConnectionFactory : Created new connection: rabbitConnectionFactory#2c104774:0/SimpleConnection@2e8ab815 [delegate=amqp://paran@127.0.0.1:5672/, localPort= 50415]
2025-04-19 16:27:04.910 INFO 11324 --- [ main] com.paran.App : Started App in 1.032 seconds (JVM running for 1.604)
消费者1接收到direct.queue1的消息:【Hello 123】
消费者1接收到direct.queue1的消息:【Hello 1234567】
5. 配置JSON转换器
消息不止可以发送String类型的,也可以发送任意类型的Java对象,在数据传输时,它会把你发送的消息序列化为字节发送给MQ,接收消息的时候,还会把字节反序列化为Java对象。只不过,默认情况下Spring采用的序列化方式是JDK序列化。JDK的序列化存在很多弊端,所以这里要换一换。
默认情况发送object,在控制台看到的消息格式非常不友好

在publisher和consumer中引入依赖
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.9.10</version>
</dependency>
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyConfig {
@Bean
public MessageConverter messageConverter(){
// 1.定义消息转换器
Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter();
// 2.配置自动创建消息id,用于识别不同消息,也可以在业务中基于ID判断是否是重复消息
jackson2JsonMessageConverter.setCreateMessageIds(true);
return jackson2JsonMessageConverter;
}
}
配置好消息转换器之后,我们就可以直接发个自定义的对象试试了
@Test
public void testSimpleQueue() {
String exchangeName= "new.direct";
rabbitTemplate.convertAndSend(exchangeName,"blue",Person.builder().age(18).name("paran").build());
}

在@RabbitListener修饰的方法那里,入参可以写rabbitTemplate.convertAndSend里发送的类Person,也可以用Message(org.springframework.amqp.core.Message)来接,实际上在发送的时候也是先转成Message后发的
如果用Message来接的话,可以用getMessageProperties().getMessageId()来获取MessageId(如果在配置消息转换器的时候设置了jackson2JsonMessageConverter.setCreateMessageIds(true);)
如果获取消息体,可以用getBody(),这时候就是我们的Person对象了
169万+

被折叠的 条评论
为什么被折叠?



