什么是MQ?
Message Queen(MQ)消息队列
为了解决分布式系统之间的通信问题。
我们原本的单体架构应用就好比一家人在一起干活,而现在的分布式架构应用就可以理解为分家了,每个人分开干活,但是这每个人之间必须可以通信才可以更好的干活。
分布式系统通信的模式有两种:直接调用模块,借助第三方完成通信。而我们MQ就是第三方,消息先发给MQ,再由MQ发给其他模块服务。
RabbitMq架构图
publisher
:生产者,也就是发送消息的一方
consumer
:消费者,也就是消费消息的一方
queue
:队列,存储消息。生产者投递的消息会暂存在消息队列中,等待消费者处理
exchange
:交换机,负责消息路由。生产者发送的消息由交换机决定投递到哪个队列。
virtual host
:虚拟主机,起到数据隔离的作用。每个虚拟主机相互独立,有各自的exchange、queue
为什么要学MQ?
1、应用解耦
用户发起下订单的请求,请求先来到订单系统,下订单需要调用支付系统完成支付操作,调用库存系统更新库存,调用物流系统更新物流信息。
而如果没有用消息队列,订单系统直接耦合调用这三个系统的话,如果有一个系统出错了,就会导致下订单出错。
现在有了MQ消息队列的话,订单系统就和其他系统分开,解耦合。订单系统接收到下订单的请求后只需要将请求消息发给消息队列中后,就可以返回下订单成功的消息给用户, 后续的支付、库存、物流等操作,由MQ将消息发给他们,让他们完成相应的操作,就算其中有个系统出错了,过一段时间好了也会继续完成工作的。就是订单下完直接返回成功,后面的操作肯定早晚会完成的。
2、异步提速
在没有用MQ之前,用户下订单后,下订单需要先调用库存系统更新库存后,再调用支付系统完成支付操作,最后调用调用物流系统更新物流信息,这些操作是同步完成的,用户要一直等这些操作完成才能完成下订单操作。要花20+300+300+300=920ms
可见请求处理的速度大大提升了,而且单位时间处理的请求数也变多了(系统吞吐量)。
有了MQ之后,用户下订单,订单系统只需要将消息发给MQ然后保存数据到数据库后,就可以返回下单成功的信息,至于后面的库存、支付、物流的操作,后面它们自己可以从MQ中取消息完成操作,对于客户来说是透明的。只需要花20+5=25ms
3、消峰填谷
举个例子,如A系统每秒最多能处理1000次请求,这个处理能力应付正常时段绰绰有余,正常时段我们发送请求一秒后就能返回结果。但是在高峰期,如果一秒钟之内有5000请求发给A系统,这时候A系统就会直接坏掉,这样就不行。使用消息队列做缓冲,把5000个请求都发给MQ,这些请求消息依次在MQ中排队,先到先出来发给A系统消费,每秒发送1000个请求给A系统处理。
对于A系统来说就是消峰了,从5000次请求消到1000次。大量请求数据堆积在MQ中,直到消费完堆积的消息,就叫填谷。
安装
Erlang和RabbitMq版本关系:Erlang Version Requirements | RabbitMQ
安装问题
解决rabbitmq启动报错“node ‘rabbit‘ not running at all”_node 'rabbit' not running at all-CSDN博客
卸载RabbitMq使用管理员安装
linux
docker run \
-e RABBITMQ_DEFAULT_USER=itheima \
-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
RabbitMq生产者
RabbitMq交换机
负责将收到来自生产者的消息路由发送给对应的消息队列,如果路由失败,消息将丢失,交换机不存储消息。
Fanout
-
Fanout:广播,将消息交给所有绑定到交换机的队列。
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 {
/**
* 声明交换机
* @return Fanout类型交换机
*/
@Bean
public FanoutExchange fanoutExchange(){
return new FanoutExchange("hmall.fanout");
}
/**
* 第1个队列
*/
@Bean
public Queue fanoutQueue1(){
return new Queue("fanout.queue1");
}
/**
* 绑定队列和交换机
*/
@Bean
public Binding bindingQueue1(Queue fanoutQueue1, FanoutExchange fanoutExchange){
return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
}
/**
* 第2个队列
*/
@Bean
public Queue fanoutQueue2(){
return new Queue("fanout.queue2");
}
/**
* 绑定队列和交换机
*/
@Bean
public Binding bindingQueue2(Queue fanoutQueue2, FanoutExchange fanoutExchange){
return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
}
}
消息发送和接收
@Test
public void testFanoutExchange() {
// 交换机名称
String exchangeName = "hmall.fanout";
// 消息
String message = "hello, everyone!";
rabbitTemplate.convertAndSend(exchangeName, "", message);
}
@RabbitListener(queues = "fanout.queue1")
public void listenFanoutQueue1(String msg) {
System.out.println("消费者1接收到Fanout消息:【" + msg + "】");
}
@RabbitListener(queues = "fanout.queue2")
public void listenFanoutQueue2(String msg) {
System.out.println("消费者2接收到Fanout消息:【" + msg + "】");
}
Direct
-
Direct:订阅,基于RoutingKey(路由key)发送给订阅了消息的队列
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 {
/**
* 声明交换机
* @return Direct类型交换机
*/
@Bean
public DirectExchange directExchange(){
return ExchangeBuilder.directExchange("hmall.direct").build();
}
/**
* 第1个队列
*/
@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");
}
/**
* 第2个队列
*/
@Bean
public Queue directQueue2(){
return new Queue("direct.queue2");
}
/**
* 绑定队列和交换机
*/
@Bean
public Binding bindingQueue2WithRed(Queue directQueue2, DirectExchange directExchange){
return BindingBuilder.bind(directQueue2).to(directExchange).with("red");
}
/**
* 绑定队列和交换机
*/
@Bean
public Binding bindingQueue2WithYellow(Queue directQueue2, DirectExchange directExchange){
return BindingBuilder.bind(directQueue2).to(directExchange).with("yellow");
}
}
发送消息和接收消息
@Test
public void testSendDirectExchange() {
// 交换机名称
String exchangeName = "hmall.direct";
// 消息
String message = "红色警报!日本乱排核废水,导致海洋生物变异,惊现哥斯拉!";
// 发送消息
rabbitTemplate.convertAndSend(exchangeName, "red", message);
}
@RabbitListener(queues = "direct.queue1")
public void listenDirectQueue1(String msg) {
System.out.println("消费者1接收到direct.queue1的消息:【" + msg + "】");
}
@RabbitListener(queues = "direct.queue2")
public void listenDirectQueue2(String msg) {
System.out.println("消费者2接收到direct.queue2的消息:【" + msg + "】");
}
Topic
-
Topic:通配符订阅,与Direct类似,只不过RoutingKey可以使用通配符。(可以实现广播和订阅)
通配符规则:
#
:匹配一个或多个词
*
:匹配不多不少恰好1个词举例:
item.#
:能够匹配item.spu.insert
或者item.spu
item.*
:只能匹配item.spu
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue1"),
exchange = @Exchange(name = "hmall.topic", type = ExchangeTypes.TOPIC),
key = "china.#"
))
public void listenTopicQueue1(String msg){
System.out.println("消费者1接收到topic.queue1的消息:【" + msg + "】");
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue2"),
exchange = @Exchange(name = "hmall.topic", type = ExchangeTypes.TOPIC),
key = "#.news"
))
public void listenTopicQueue2(String msg){
System.out.println("消费者2接收到topic.queue2的消息:【" + msg + "】");
}
消息发送和接收
/**
* topicExchange
*/
@Test
public void testSendTopicExchange() {
// 交换机名称
String exchangeName = "hmall.topic";
// 消息
String message = "喜报!孙悟空大战哥斯拉,胜!";
// 发送消息
rabbitTemplate.convertAndSend(exchangeName, "china.news", message);
}
@RabbitListener(queues = "topic.queue1")
public void listenTopicQueue1(String msg){
System.out.println("消费者1接收到topic.queue1的消息:【" + msg + "】");
}
@RabbitListener(queues = "topic.queue2")
public void listenTopicQueue2(String msg){
System.out.println("消费者2接收到topic.queue2的消息:【" + msg + "】");
}
-
Headers:头匹配,基于MQ的消息头匹配,用的较少。
RabbitMq消费者
手动确认消息
确认消息
void basicAck(long deliveryTag, boolean multiple) throws IOException;
手动确认消息 第一个参数是该消息的唯一标示值 false表示只删除该消息(true表示确认所有比指定的 deliveryTag 小或等于它的消息。)
拒绝消息
channel.basicNack(long deliveryTag, boolean multiple, boolean requeue):
deliveryTag:要拒绝的消息的唯一标识。
multiple:一个布尔值,如果设置为 true,则会拒绝该 deliveryTag 之前的所有未确认消息。
requeue:一个布尔值,如果设置为 true,则拒绝的消息会重新入队列,即放回到队列中,以便其他消费者再次处理。
使用 channel.basicNack 方法,你可以选择拒绝一条或多条消息,并选择是否重新入队。channel.basicReject(long deliveryTag, boolean requeue):
deliveryTag:要拒绝的消息的唯一标识。
requeue:一个布尔值,如果设置为 true,则拒绝的消息会重新入队列,即放回到队列中,以便其他消费者再次处理。
使用 channel.basicReject 方法,你只能拒绝一条消息,但你可以选择是否重新入队。
RabbitMq机制
持久化
将消息和队列标记为持久化,旨在确保消息在 RabbitMQ 重启或故障时不会丢失。
持久化消息是指将消息数据存储到磁盘中,而不仅仅保存在内存中。
持久化队列是指队列的元数据被保存在磁盘中,以便 RabbitMQ 重启后能够恢复队列状态。
ps:将一个持久化消息发布到一个持久化队列中时,当RabbitMq重启时,持久化队列和持久化消息都会被恢复,确保消息不会丢失。
消息确认
confirm
由 RabbitMQ 发送给生产者的确认,表明消息已成功写入队列,用来保障生产者可靠的消息发送。
生产者在发送消息后,可以开启 confirm
模式,RabbitMq成功接收消息并存储后才会向生产者发送confirm确认。
ack
由消费者发送的确认,表明该消息已被成功处理,用来确保消费者可靠的处理消息。
消费者处理完消息,向RabbitMq发生ack,只有在收到 ack
后,RabbitMQ 才会删除该消息。可以设置为手动或自动确认。
retry
在 RabbitMQ 中,重试(retry)机制用于处理消费者处理消息失败的情况,以确保消息能够被重新尝试消费。
重试机制可以帮助确保消息最终会被成功处理,提高系统的可靠性。
1、使用死信队列
①当消费者处理消息失败并且达到最大重试次数后,可以将消息发送到一个死信队列。
②可以为死信队列设置 TTL(存活时间)或者使用其他策略,最终将其转发到原队列以实现重试。
2、延迟重试
可以使用 RabbitMQ 的延迟队列插件(如 rabbitmq_delayed_message_exchange
),或通过创建一个延迟队列。
消息会被发送到延迟队列,在设置的延迟时间后,再转发到原队列。
消息TTL
设置消息的存活时间Time-To-Live,超时后会被自动删除
死信队列
用于处理未成功消费的消息,提供错误处理机制。
流量控制
防止消费者处理速度过慢导致内存占用过高。
集群(Clustering):
多个 RabbitMQ 实例可以组成一个集群,以提高可用性和扩展性。