MQ基础
同步调用存在的问题
- 耦合度高:每次加入新的需求,都要修改原来的代码
- 性能下降:调用者需要等待服务提供者响应,如果嗲用链过长则相应时间等于每次调用的之间之和
- 资源浪费:调用链中的每个服务在等待响应过程中,不能释放请求占用的资源,高并发场景下会极度浪费系统资源
- 级联失败:如果服务提供者出现问题,所有调用方都会跟着出问题
异步调用常见实现就是事件驱动模式
当支付服务发布支付成功事件后会发送一个订单id给Broker
Broker通知服务端进行工作
通知成功后Broker通知支付服务通知成功(不需要服务端完成工作)
异步通信的优点:
- 耦合度低
- 吞吐量提升
- 故障隔离
- 流量削枫
异步通信的缺点
- 依赖于Broker的可靠性、安全性、吞吐能力
- 架构复杂,业务没有明显的刘成希那,不好追踪管理
MQ(消息队列):存放消息的队列
MQ的五种消息模型

基本消息队列

发布消息者
java
package cn.itcast.mq.helloworld;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import org.junit.Test;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class PublisherTest {
@Test
public void testSendMessage() throws IOException, TimeoutException {
// 1.建立连接
ConnectionFactory factory = new ConnectionFactory();
// 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
factory.setHost("192.168.150.101");
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("itcast");
factory.setPassword("123321");
// 1.2.建立连接
Connection connection = factory.newConnection();
// 2.创建通道Channel
Channel channel = connection.createChannel();
// 3.创建队列
String queueName = "simple.queue";
channel.queueDeclare(queueName, false, false, false, null);
// 4.发送消息
String message = "hello, rabbitmq!";
channel.basicPublish("", queueName, null, message.getBytes());
System.out.println("发送消息成功:【" + message + "】");
// 5.关闭通道和连接
channel.close();
connection.close();
}
}
消费者
java
package cn.itcast.mq.helloworld;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class ConsumerTest {
public static void main(String[] args) throws IOException, TimeoutException {
// 1.建立连接
ConnectionFactory factory = new ConnectionFactory();
// 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
factory.setHost("192.168.150.101");
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("itcast");
factory.setPassword("123321");
// 1.2.建立连接
Connection connection = factory.newConnection();
// 2.创建通道Channel
Channel channel = connection.createChannel();
// 3.创建队列
String queueName = "simple.queue";
channel.queueDeclare(queueName, false, false, false, null);
// 4.订阅消息
channel.basicConsume(queueName, true, new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException {
// 5.处理消息
String message = new String(body);
System.out.println("接收到消息:【" + message + "】");
}
});
System.out.println("等待接收消息。。。。");
}
}
SpringAMQP
AMQP是用于在应用程序或之间传递业务消息的开放标准。该协议与语言和平台无关,丰富和微服务中独立性的要求
SpringAMQP:基于AMQP协议定义的一套API规范,提供了模板来发送和接收消息。包含两部分,其中spring-amqp是基础抽象,spring-rabbit是底层的默认实现
坐标
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
配置添加mq连接信息
java
spring:
rabbitmq:
host: 192.168.150.101 # 主机名
port: 5672 # 端口
virtual-host: / # 虚拟主机
username: itcast # 用户名
password: 123321 # 密码
发送消息
java
package cn.itcast.mq.spring;
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void test(){
String queueName = "simple.queue"; // 队列名称
String message = "hello"; // 消息
rabbitTemplate.convertAndSend(queueName,message);
}
}
消费者编写监听器监听队列消息
java
package cn.itcast.mq;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class SpringRabbitListener {
@RabbitListener(queues = "simple.queue")
public void listenSimpleQueue(String msg){
System.out.println(msg);
}
}
WorkQueue模型
消息预取机制:消费者会提前将消息取走,等待后期慢慢处理
配置预取数量限制
java
spring:
rabbitmq:
host: 192.168.150.101 # 主机名
port: 5672 # 端口
virtual-host: / # 虚拟主机
username: itcast # 用户名
password: 123321 # 密码
listener:
simple:
prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息
发布订阅模式
这个交换机在接收到消息后,会直接转发到绑定到它上面的所有队列。
实现思路如下:
- 在consumer服务中,利用代码声明队列、交换机,并将两者绑定
- 在consumer服务中,编写两个消费者方法,分别监听fanout.queue1和fanout.queue2
- 在publisher中编写测试方法,向itcast.fanout发送消息
声明队列、交换机,并将两者绑定
java
package cn.itcast.mq;
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 {
// 声明FanoutExchange交换机
@Bean
public FanoutExchange fanoutExchange(){
return new FanoutExchange("itcast.fanout");
}
// 声明第1个队列
@Bean
public Queue fanoutQueue1(){
return new Queue("fanout.queue1");
}
//绑 定队列1和交换机
@Bean
public Binding bindingQueue1(Queue fanoutQueue1, FanoutExchange fanoutExchange){
return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
}
@Bean
public Queue fanoutQueue2(){
return new Queue("fanout.queue2");
}
//绑 定队列2和交换机
@Bean
public Binding bindingQueue2(Queue fanoutQueue2, FanoutExchange fanoutExchange){
return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
}
}
消费者代码不变
发送消息
java
@Test
public void test2(){
// 交换机名称
String exchangeName = "itcast.fanout"; // 指定交换机
String message = "hello"; // 消息
// 将消息发送给所有消费者
rabbitTemplate.convertAndSend(exchangeName,"",message);
}
DirectExchange(路由)
DirectExchange会将接收到的消息根据规则路由到指定的队列中,称为路由模式
- 每个队列都会与Exchange设置一个BindingKey
- 发布消息时指定RoutingKey
- Exchange将消息路由到BindingKey与消息RoutingKey一致的队列
实现思路如下:
- 利用@RabbitListener声明Exchange、Queue、RoutingKey
- 在consumer服务中,编写两个消费者方法,分别监听direct.queue1和direct.queue2
- 在publisher中编写测试方法,向itcast.direct发送消息
绑定交换机
java
package cn.itcast.mq;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FanoutConfig {
// 声明FanoutExchange交换机
@Bean
public FanoutExchange fanoutExchange(){
return new FanoutExchange("itcast.fanout");
}
// 声明第1个队列
@Bean
public Queue fanoutQueue1(){
return new Queue("fanout.queue1");
}
//绑 定队列1和交换机
@Bean
public Binding bindingQueue1(Queue fanoutQueue1, FanoutExchange fanoutExchange){
return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
}
@Bean
public Queue fanoutQueue2(){
return new Queue("fanout.queue2");
}
//绑 定队列2和交换机
@Bean
public Binding bindingQueue2(Queue fanoutQueue2, FanoutExchange fanoutExchange){
return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
}
@RabbitListener(bindings = @QueueBinding(
value = @org.springframework.amqp.rabbit.annotation.Queue, // 队列名称
exchange = @Exchange(name = "itcast.direct",type = ExchangeTypes.DIRECT), // 交换机
key = {"red", "yellow"} //key
))
public void listenDirectQueue2(String msg){
System.out.println("消费者2接收到Direct消息:【"+msg+"】 ");
}
}
发送消息
java
@Test
public void test2(){
// 交换机名称
String exchangeName = "itcast.fanout"; // 指定交换机
String message = "hello"; // 消息
rabbitTemplate.convertAndSend(exchangeName,"blue",message); //blue 是routingkey
}
接收消息不变
TopicExchange
TopicExchange和DirectExchange类似,但是TopicExchange的routingkey必须由多个单词的列表组成,中间用"."隔开
Queue与Exchange指定Bindingkey可以使用通配符
#:代表0各或多个单词
*:代表一个单词
消息转换
Spring的对消息对象的处理是由org.springframework.amqp.support.converter.MessageConverter来处理的。而默认实现是SimpleMessageConverter,基于JDK的ObjectOutputStream完成序列化。
如果要修改只需要定义一个MessageConverter 类型的Bean即可。推荐用JSON方式序列化,步骤如下:
导入坐标(生产者消费者都要引入)
xml
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
配置类声明MessageConverter:(服务端和消费端都要配置)
java
@Bean
public MessageConverter jsonMessageConverter(){
return new Jackson2JsonMessageConverter();
}
生产者可靠性
生产者重连:客户端可能会连接MQ失败,通过配置可以开启连接失败后重连机制
yml
spring:
rabbitmq:
connection-timeout: 1s # 设置MQ的连接超时时间
template:
retry:
enabled: true # 是否开启超时重试机制
initial-interval: 1000ms # 失败后的初始等待时间
multiplier: 1 # 失败后下次的等待时长倍数
max-attempts: 3 # 最大重试次数
生产者确认:MQ由publisher Confirm和Publisher Return两种确认机制,开启确认机制后,在MQ成功收到消息后会返回确认消息给生产者,返回的结果有一下几种情况:
- 消息投递到了MQ,但是路由失败,此时会通过Publisher Return返回路由异常原因,然后返回ACK,告知投递成功
- 临时消息投递到MQ,并且入队成功,返回ACK,告知投递成功
- 持久消息投递到了MQ,并且入队完成持久化,返回ACK,告知ACK投递成功
- 其他情况都会返回NACK告知投递失败
yml
spring:
rabbitmq:
publisher-confirm-type: correlated # 开启public confirm机制
publisher-returns: true # 开启return机制
publisher-confirm-typ有三种模式:
- none:关闭confirm机制
- simple:同步阻塞等待MQ的回执信息
- correlated :MQ异步回调方式,返回回执消息
ReturnCallback
java
@Slf4j
@Configuration
public class CommonConfig implements ApplicationContextAware {
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
// 获取RabbitTemplate
RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
// 设置ReturnCallback,每个获取RabbitTemplate只能配置一个设置ReturnCallback
rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
@Override
public void returnedMessage(ReturnedMessage returnedMessage) {
log.debug("受到消息的return callback",returnedMessage.getRoutingKey(),returnedMessage.getMessage(),
returnedMessage.getExchange(),returnedMessage.getReplyCode(),returnedMessage.getReplyText());
}
});
}
}
ConfirmCallback
java
public void testConfirmCallback() {
// 1. 创建CorrelationData对象(包含唯一ID)
CorrelationData cd = new CorrelationData(UUID.randomUUID().toString());
// 2. 添加ConfirmCallback回调
cd.getFuture().addCallback(new ListenableFutureCallback<CorrelationData.Confirm>() {
@Override
public void onFailure(Throwable ex) {
// 消息回调失败时的处理
log.error("消息回调失败", ex);
}
@Override
public void onSuccess(CorrelationData.Confirm result) {
// 收到Broker确认时的处理
log.debug("收到confirm callback回执");
if (result.isAck()) {
// 消息发送成功(Broker已确认)
log.debug("消息发送成功,收到ack");
} else {
// 消息发送失败(Broker拒绝)
log.error("消息发送失败,收到nack,原因:{}", result.getReason());
}
}
});
rabbitTemplate.convertAndSend("hhh","red","gello",cd);
}
MQ的可靠性
在默认情况下,Rabbit MQ会将接收到的信息保存在内存中以降低消息收发的延迟,这会导致两个问题:
- 一旦MQ宕机,内存中的消息会丢失
- 内存空间存在上限,当消费者故障或处理过慢时,会导致消息积压,引发MQ阻塞
解决方法:
- 数据持久化
- LazyQueue(常用)
数据持久化
Rabbit MQ数据持久化的三个方面:
交换机持久化
队列持久化
消息持久化:将消息同时写到磁盘和内存中
spring默认将交换机和队列设置会持久化的
LazyQueue(惰性队列)
接收到消息后直接存入磁盘而非内存(内存中只保留最近的消息)
消费者要消费消息时才会从磁盘中读取并加载到内存
支持数百万条的消息存储
消费者可靠性
为了确认消费者是否成功处理消息,Rabbit MQ提供了消费者确认机制。
当消费者处理消息结束后,应该向Rabbit MQ发送一个回执,告诉RabbitMQ自己消息处理状态,回执有三种:
- ack:成功处理消息,RabbitMQ从队列中删除该消息
- nack:消息处理失败,Rabbit MQ需要再次投递信息
- reject:消息处理失败并拒绝该消息,Rabbit MQ从队列中删除该消息
SpringAMQP已经实现了消息确认功能。并允许我们通过配置文件,选择ACK处理方式,有三种方式:
- none:不处理,即消息投递给消费者后立即ack,消息会立刻从MQ中删除,非常不安全,不建议使用
- manual:手动模式。需要自己在业务代码中调用api,发送ack或reject,存在业务入侵,更灵活
- auto:自动模式。SpringAMQP利用AOP对我们的消息处理逻辑做了环绕增强,当业务正常执行时则自动返回ack,当业务出现异常时,会根据异常判断返回不同结果
- 如果时业务异常,自动返回nack
- 如果时消息处理或发生异常,会自动返回reject
开启auto模式
yml
spring:
rabbitmq:
listener:
simple:
prefetch: 1
acknowledge-mode: auto
失败重试机制:当消费者出现异常后,消息会不断requeue(重新入队),再重新发送给消费者,然后再次异常,再次requeue,无限循环,导致mq的消息处理飙升,带来不必要的压力
我们可以利用spring的retry机制,再消费者出现异常时利用本地重试,而不是无限的requeue到mq队列
yml
spring:
rabbitmq:
listener:
simple:
prefetch: 1 # 每次取一条
acknowledge-mode: auto
retry:
multiplier: 1 # 下次失败的等待时长背书
max-attempts: 3 # 最大重试次数,重试三次后将雄安锡丢掉
stateless: true # true无状态;false有状态。如果业务中包含事务,选择false
开启重试模式后,重试次数耗尽如果消息依然失败,则需要有MessageRecoverer接口来处理,她包含三种不同的实现:
- RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认为这种方式
- ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队
- RepublishMessageRecoverer:重试耗尽后,将失败的消息投递到指定的交换机
业务幂等性
业务幂等性:保证重复提交一个业务时只会执行一次
方法一:给每个消息都设置一个唯一id,利用id区分是否是重复消息
- 每一条消息都生成一个唯一id和消息一起投递给消费者
- 消费者接收到消息后处理自己的业务,业务处理成功后将消息ID保存到数据库
- 如果下次又收到相同消息,取数据库查询判断是否存在
方法二:结合业务逻辑,基于业务本身做判断
延迟消息
延迟消息:生产者发送消息时指定一个时间,消费者不会立刻收到消息,而是在指定时间之后才收到消息
延迟任务:设置在一定时间之后才执行的任务
死信交换机
死信(Dead Letter):是指在消息队列系统中那些无法被正常消费的消息。
死信队列(Dead Letter Queue, DLQ):当消息无法被正常处理时,也就是死信,可以将这些死信发送到一个专门的队列中,以便于后续检查和处理
当一个队列中的消息满足下列情况之一时,就会成为死信:
- 消费者使用basic.reject或者basic.nack声明消费失败,并且消息的requeue参数设置为false
- 消息时一个过期消息(达到了队列或消息本身设置的过期时间),超时无人消费
- 要投递的队列消息堆积满了,最早的消息可能称为死信
如果队列通过dead-letter-exchange属性指定了一个交换机,那么该队列中的死信就回投递到这个交换机中。这个交换机称为死信交换机
延迟消息插件:RabbitMQ推出了一个插件,原生支持延迟消息功能。该插件的原理是设计了一种支持延迟消息功能的交换机,当消息投递到交换机后可以暂存一定时间,到期后再投递到队列
设置30分钟后检测订单支付状态实现非常简单,存在两个问题
- 如果并发较高,30分钟可能堆积消息过多,对MQ压力很大
- 大多数订单再下单后1分钟内就回支付,但是MQ内等待30分钟,浪费资源