1. 什么是MQ
消息队列(Message Queue,简称MQ)
- 从字面意思上看,本质是个队列,FIFO先入先出,只不过队列中存放的内容是message而已。
其主要用途:不同进程Process/线程Thread之间通信。
为什么会产生消息队列?有几个原因:
-
不同进程(process)之间传递消息时,两个进程之间耦合程度过高,改动一个进程,引发必须修改另一个进程,为了隔离这两个进程,在两进程间抽离出一层(一个模块),所有两进程之间传递的消息,都必须通过消息队列来传递,单独修改某一个进程,不会影响另一个;
-
不同进程(process)之间传递消息时,为了实现标准化,将消息的格式规范化了,并且,某一个进程接受的消息太多,一下子无法处理完,并且也有先后顺序,必须对收到的消息进行排队,因此诞生了事实上的消息队列;
MQ框架非常的多,比较流行的有RabbitMq、kafka,以及阿里开源的RocketMQ。本文主要介绍RabbitMq。
区别
| 优点 | 缺点 | 适用场景 | |
|---|---|---|---|
| kafka | 吞吐量非常大,性能非常好,技术生态完整 | 功能比较单一 | 分布式日志搜集、大数据采集,linkin用来处理分布式日志用的 |
| RabbitMQ | 消息可靠性强,功能全面 | 吞吐量较低。消息挤压会影响性能,erlang语言比较小众 | 企业内部系统调用 |
| RocketMQ | 高吞吐、高性能、高可用、高级功能非常全 | 技术生态不是很完整 | 几乎全场景,尤其适合金融 ,阿里拿来做金融用的 |
2. 为什么使用消息队列
主要有三个作用:
-
解耦。如图所示。本身生产者所需要的数据被消费者们所需要,这要给他们都添加响应的方法,但如果消费者3突然不需要该信息,生产者就要删掉该方法,但如果增加这个中间层进行解耦,就可以让他们只去找这个消息队列去拿内容,而不需要再次去寻找生产者,生产者可以更专注于自己的业务。

-
异步。如图所示。一个客户端请求发送进来,系统A会调用系统B、C、D三个系统,同步请求的话,响应时间就是系统A、B、C、D的总和,也就是800ms。如果使用MQ,系统A发送数据到MQ,然后就可以返回响应给客户端,不需要再等待系统B、C、D的响应,可以大大地提高性能。对于一些非必要的业务,比如发送短信,发送邮件等等,就可以采用MQ。

-
削峰。就是当业务量大的时候,让他做一个桥的作用,不让数据一股脑打到数据库上,让数据库宕机。使用MQ,是让sql语句不在直接打到数据库,而是把数据发送到MQ,MQ短时间积压数据是可以接受的,然后由消费者每次拉取2000条进行处理,防止在请求峰值时期大量的请求直接发送到MySQL导致系统崩溃。
3. RabbitMQ介绍
RabbitMQ 是一个开源的消息代理和队列服务器,用于在分布式系统中存储、转发和接收消息。它是基于高级消息队列协议(AMQP)实现的,支持多种客户端和协议,可以用于多种场景,如负载均衡、分布式事务处理、消息通知等。
RabbitMQ 的核心概念
RabbitMQ 的核心概念包括生产者、消费者、队列、交换机和绑定。生产者负责发送消息到交换机,交换机根据路由规则将消息转发到绑定的队列,消费者从队列中获取消息进行处理。RabbitMQ 支持多种类型的交换机,如直接交换机(direct)、扇形交换机(fanout)、主题交换机(topic)和头交换机(headers),它们各自适用于不同的路由策略和模式。
RabbitMQ 的工作原理
RabbitMQ 的工作原理是通过Broker(消息代理服务器)来接收、存储和转发消息。Broker 包含一个或多个虚拟主机,每个虚拟主机可以有自己的队列、交换机和绑定。消息的生产者将消息发送到交换机,交换机根据绑定规则将消息路由到队列,消费者监听队列并处理消息。
RabbitMQ 的使用场景
RabbitMQ 可以用于实现系统间的解耦、异步处理和流量削峰。例如,在电商系统中,订单生成后可以将订单信息发送到消息队列,库存系统和物流系统可以从队列中获取订单信息进行处理,这样即使某个系统暂时不可用,也不会影响整个流程的进行。此外,RabbitMQ 还可以用于实现延迟消息和定时任务,如订单超时未支付自动取消等功能。
4. 使用
RabbitMQ是由ErLang开发的,他需要erlang的环境来进行运行。
所以要先安装erlang,你可以根据rabbitMQ官网来看erlang适应的版本。

安装好erlang之后,将erlang配置成环境变量,也就是将erlang的sbin目录保存在环境变量的path中。
然后官网安装windows包

这是安装好的目录。

安装完之后,他会自动执行,这里我出现了一个错误,就是他自动运行之后,并没有运行起来,我怀疑是windows的运行方式和其他的不一样。
那我是如何解决的呢,就是先通过windows的services.msc进入服务,关闭自动运行的rabbitMQ,然后调整成手动运行,之后在执行rabbitMQ的bat命令
运行命令
java
rabbitmq-server start
当你看见下面情况时候

证明运行成功了。
然后你可以访问web页面,http://localhost:15672
账号密码默认是:guest/guest
进入到下面页面,就大功告成了。

用户
查看当前拥有用户
java
rabbitmqctl list_users
查看权限
java
rabbitmqctl list_permissions
添加用户
java
rabbitmqctl add_user user_name password
设置用户tag
java
rabbitmqctl set_user_tags user_name administrator
设置用户权限
java
rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"
5. java操作
当然你的服务配置好,并没有结束,而是刚刚开始,我们需要用编程语言去操作他,这里我使用java。
首先引入环境,springboot是拥有mq环境的,所以只需要引入即可。
pom
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
依然是在配置yml中加入RabbitMQ的配置信息
yml
spring:
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
然后是创建RabbitMQ的配置类,放入到IOC当中。
java
@Configuration
public class DirectRabbitConfig {
@Bean
public Queue rabbitmqDemoDirectQueue() {
/**
* 1、name: 队列名称
* 2、durable: 是否持久化
* 3、exclusive: 是否独享、排外的。如果设置为true,定义为排他队列。则只有创建者可以使用此队列。也就是private私有的。
* 4、autoDelete: 是否自动删除。也就是临时队列。当最后一个消费者断开连接后,会自动删除。
* */
return new Queue(RabbitMQConfig.RABBITMQ_DEMO_TOPIC, true, false, false);
}
@Bean
public DirectExchange rabbitmqDemoDirectExchange() {
//Direct交换机
return new DirectExchange(RabbitMQConfig.RABBITMQ_DEMO_DIRECT_EXCHANGE, true, false);
}
@Bean
public Binding bindDirect() {
//链式写法,绑定交换机和队列,并设置匹配键
return BindingBuilder
//绑定队列
.bind(rabbitmqDemoDirectQueue())
//到交换机
.to(rabbitmqDemoDirectExchange())
//并设置匹配键
.with(RabbitMQConfig.RABBITMQ_DEMO_DIRECT_ROUTING);
}
}
然后是你运行操作数据所需要的服务类
java
@Service
public class RabbitMQServiceImpl implements RabbitMQService {
//日期格式化
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Resource
private RabbitTemplate rabbitTemplate;
@Override
public String sendMsg(String msg) throws Exception {
try {
String msgId = UUID.randomUUID().toString().replace("-", "").substring(0, 32);
String sendTime = sdf.format(new Date());
Map<String, Object> map = new HashMap<>();
map.put("msgId", msgId);
map.put("sendTime", sendTime);
map.put("msg", msg);
rabbitTemplate.convertAndSend(RabbitMQConfig.RABBITMQ_DEMO_DIRECT_EXCHANGE, RabbitMQConfig.RABBITMQ_DEMO_DIRECT_ROUTING, map);
return "ok";
} catch (Exception e) {
e.printStackTrace();
return "error";
}
}
}
削峰填谷
在高并发场景下,例如秒杀活动,用户请求量可能瞬间激增,导致服务器无法承受。RabbitMQ 通过消息队列的方式,将瞬时流量缓冲到队列中,然后以稳定的速率处理这些请求,从而实现削峰填谷。
配置文件示例
以下是一个 Spring Boot 项目的 RabbitMQ 配置文件示例:
java
spring.application.name=springboot_rabbitmq
spring.rabbitmq.host=192.168.0.102
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=admin
spring.rabbitmq.virtual-host=/
spring.rabbitmq.listener.simple.acknowledge-mode=manual # 设置手动应答
spring.rabbitmq.listener.simple.prefetch=2 # 每次最多可处理信息量
消费者代码示例
消费者代码通过 @RabbitListener 注解监听队列,并手动应答消息:
java
@Component
@RabbitListener(queuesToDeclare = @Queue(name = "springboot-limit"))
public class CurrentlimitCustomer {
@RabbitHandler
public void receive(String msg, Channel channel, Message message) throws IOException {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
Thread.sleep(1000 * 10); // 模拟处理时间
System.out.println("=====限流====>");
System.out.println(msg);
System.out.println(channel);
System.out.println(message);
// 手动签收消息
channel.basicAck(deliveryTag, true);
} catch (Exception e) {
// 处理异常
}
}
}
生产者代码示例
生产者代码通过 rabbitTemplate 发送消息到队列:
java
@Test
public void test09() throws Exception {
for (int i = 0; i < 10; i++) {
rabbitTemplate.convertAndSend("springboot-limit", "限流测试");
}
Thread.sleep(1000 * 1000); // 模拟延迟
}
削峰填谷的优势
- 提高系统稳定性:通过缓冲瞬时流量,避免系统过载。
- 提升用户体验:减少请求失败的概率,提高系统响应速度。
- 简化系统设计:通过消息队列实现异步处理,降低系统耦合度。
RabbitMQ 的削峰填谷功能在处理高并发场景中具有显著优势,能够有效提高系统的稳定性和可用性。
交换机
Exchange交换机是消息路由的核心组件,主要用于接收生产者发送的消息,并根据路由键(Routing Key)将消息分发到绑定的队列(Queue)。它在消息队列系统(如RabbitMQ)中起到类似"邮局"的作用,负责将消息分拣到正确的队列,从而实现生产者与消费者的解耦。
交换机的核心功能
生产者将消息发送到交换机,而非直接发送到队列。交换机根据绑定规则(Binding Key)和路由键将消息转发到对应的队列。这样,生产者无需关心消息的具体消费队列,消费者也无需了解消息的来源。
交换机的类型
交换机有四种主要类型,每种类型适用于不同的场景:
1. Direct直连交换机
就如同他的名字,直接将内容放入到指定的queue队列。消息通过完全匹配路由键转发到绑定的队列。适用于点对点精确路由场景,例如订单系统根据订单ID分发消息。
java
String queueName = "hello.queue1";
rabbitTemplate.convertAndSend(queueName,"hello,everyone");
或者指定routingKey来发送
java
String exchangeName = "hello.direct";
rabbitTemplate.convertAndSend(exchangeName ,"nihao,everyone");
通过下图可以看到,我指定hello.queue1的routingkey为nihao,所以只会发送给hello.queue1。
是可以多个queue绑定相同的routingKey的

2. Fanout广播交换机
广播,顾名思义就是将绑定的queue都发。将消息广播到所有绑定的队列。适用于发布/订阅模式,例如系统日志广播或实时通知。
java
String exchangeName = "hello.fanout";
rabbitTemplate.convertAndSend(exchangeName ,null,"hello,everyone");
Consumer
java
package com.itheima.consumer.listener;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class RabbitMQListener {
@RabbitListener(queues = "hello.queue1")
public void onMessage(String message) {
Logger logger = LogManager.getLogger(RabbitMQListener.class);
logger.info("hello.queue1获取到的信息为{}",message);
}
@RabbitListener(queues = "hello.queue2")
public void onMessage2(String message) {
Logger logger = LogManager.getLogger(RabbitMQListener.class);
logger.info("hello.queue2获取到的信息为{}",message);
}
}
3. topic主题交换机
如果说上面的direct交换机是选择队列去发送,那这个topic交换机就是他的升级版,topic交换机可以将routingKey设置为通配符样式。
发送到类型是 topic 交换机的消息的routing key 不能随意写,必须满足一定的要求,它必须是一个单词列表,并且以点号. 分隔开 。
这些单词可以是任意单词,比如说:"stock.usd.nyse","nyse.vmw","quick.orange.rabbit" 这种类型的。当然这个单词列表最多不能超过 255 个字节。
在这个规则列表中,其中有两个替换符是特别需要注意的:
- *(星号) 可以代替一个单词,注意是一个单词,不是一个字母
- #(井号) 可以替代零个或多个单词,注意是一个单词,不是一个字母

下面代码就可以发送给这两个queue。
java
package com.itheima.publisher;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
@SpringBootTest
public class PublisherTest {
@Resource
private RabbitTemplate rabbitTemplate;
@Test
public void publisherTest(){
String exchangeName = "hello.topic";
rabbitTemplate.convertAndSend(exchangeName ,"china.news","hello,everyone");
}
}
声明队列和交换机
如果每次都在rabbit的控制台去创建队列交换机啥的,未免太麻烦了,spring-amqp为我们封装了代码创建的方法。
java
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 RabbitConfig {
@Bean
public Queue helloQueue1(){
return new Queue("hello.queue1");
}
@Bean
public Queue helloQueue2(){
return new Queue("hello.queue2");
}
@Bean
public FanoutExchange helloFanout(){
return new FanoutExchange("hello.fanout");
}
@Bean
public Binding helloBinding1(){
return BindingBuilder.bind(helloQueue1()).to(helloFanout());
}
@Bean
public Binding helloBinding2(){
return BindingBuilder.bind(helloQueue2()).to(helloFanout());
}
@Bean
public DirectExchange helloDirect(){
return new DirectExchange("hello.direct");
}
}
通过这个操作,就可以实现让服务去创建这些内容。
注解方式
amqp为我们提供了更方便的方式,注解方式 。
通过下列的操作,可以直接在监听类上完成queue和exchange的创建和绑定。

queue就是监听的队列,exchange就是队列要绑定的交换机,key就是routingKey。
java
package com.itheima.consumer.listener;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class RabbitMQListener {
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "hello.queue1",durable = "true"),
exchange = @Exchange(name = "hello.direct",type = ExchangeTypes.DIRECT),
key = {"nihao","zaijian"}
))
public void onMessage(String message) {
Logger logger = LogManager.getLogger(RabbitMQListener.class);
logger.info("hello.queue1获取到的信息为{}",message);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "hello.queue1",durable = "true"),
exchange = @Exchange(name = "hello.direct",type = ExchangeTypes.DIRECT),
key = {"china","emilia"}
))
public void onMessage2(String message) {
Logger logger = LogManager.getLogger(RabbitMQListener.class);
logger.info("hello.queue2获取到的信息为{}",message);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "hello.queue3",durable = "true"),
exchange = @Exchange(name = "hello.topic",type = ExchangeTypes.TOPIC),
key = {"china.#","#.emilia"}
))
public void onMessage3(String message) {
Logger logger = LogManager.getLogger(RabbitMQListener.class);
logger.info("hello.queue3获取到的信息为{}",message);
}
}
消息转换器
rabbitMQ自带的消息转换器是臃肿的。
我通过下面的代码向交换机发送内容。
java
package com.itheima.publisher;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.util.HashMap;
@SpringBootTest
public class PublisherTest {
@Resource
private RabbitTemplate rabbitTemplate;
@Test
public void publisherTest(){
HashMap<String, Object> map = new HashMap<>();
map.put("id","1");
map.put("name","cmc");
map.put("sex","男");
String exchangeName = "hello.topic";
rabbitTemplate.convertAndSend(exchangeName ,"i love u.emilia",map);
}
}
这个是交换机里面的结果,可以看见第二个结果是我使用了jackson的消息转换,将内容转换为了json,明显大小变小了很多,所以更加推荐使用json消息转换。

使用下面的代码,将amqp的消息转换实现注册为bean,amqp就会自动转换消息接受类型了。
java
package com.itheima.consumer.config;
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 RabbitConfig {
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
}