为什么会用到MQ呢?
答:1>在传统的系统中,当遇到相对复杂的业务中,一次用户请求可能会同步调用N个系统的接口,则需要等待所有的接口都返回了,才能真正的获取执行结果。这种同步调用接口的方式总耗时比较长,非常影响用户的体验,特别是在网络不稳定的情况下,非常容易出现接口超时问题。
2>在进行系统设计的时候,一般会将复杂的业务系统拆分成为多个子系统。例如用户下单,请求会先通过订单系统,然后分别调用:支付系统、库存系统和物流系统等系统。系统之间耦合性太高,如果调用的任何一个子系统出现异常,整个请求都会异常,对系统的稳定性非常不利。
3>秒杀活动,对系统的稳定性要求很高。如果用户比较少,则不会影响系统的稳定性。但如果用户突增,一时间所有的请求都到数据库,可能会导致数据库无法承受这么大的压力,响应变慢或者直接挂掉。
那如何解决上述三个问题呢?
答:mq
解决问题一:同步接口调用导致响应时间长的问题,使用mq之后,将同步调用改成异步,能够显著减少系统响应时间。提升用户体验和系统吞吐量(单位时间内处理请求的数目)
解决问题二:使用mq之后,我们只需要依赖于mq,避免了各个子系统间的强依赖问题。
订单系统作为消息生产者,只需要保证自己没问题就可以了,不会受到库存系统等业务子系统的异常影响,并且各个消费者业务子系统之间,也互不影响。这样就把之前复杂的业务子系统的依赖关系,转换为只依赖于mq的简单依赖,从而显著的降低了系统间的耦合度,提升容错性和可维护性。
解决问题三:业务系统接收到用户请求之后,将请求直接发送到mq,然后消费者从mq中消费消息。如果出现请求峰值的情况,由于消费者的消费能力有限,通过限流操作按照自己的节奏来消费消息,多余的消息会保留在mq的队列中,不会对系统的稳定性造成影响。
优势总结:
1>异步提速:提升用户体验和系统吞吐量
2>应用解耦:提高系统容错性和可维护性
3>削峰填谷:提高系统稳定性
劣势:
1>系统可用性降低:随着外部依赖引入的增多,系统稳定性将会越差。如果 MQ 宕机,就会对业务造成影响。(如何保证MQ的高可用?)
2>系统复杂度提高:MQ 的引入将会增加系统的复杂度,以前系统间是同步的远程调用,现在是通过 MQ 进行异步调用。(如何保证消息不被丢失等情况?)
RabbitMQ是实现了高级消息队列协议(AMQP)的开源分布式消息中间件,服务端是用Erlang语言编写的。RabbitMQ 凭借其高可靠、易扩展、高可用及丰富的功能特性,不管是互联网行业还是传统行业都在大量地使用。
RabbitMQ 基础架构如下图:
RabbitMQ 中的相关概念:
1>Broker:接收和分发消息的应用,RabbitMQ Server就是 Message Broker
2>Virtual host:出于多租户和安全因素设计的,把 AMQP 的基本组件划分到一个虚拟的分组中,类似于网络中的 namespace 概念。当多个不同的用户使用同一个 RabbitMQ server 提供的服务时,可以划分出多个vhost,每个用户在自己的 vhost 创建 exchange/queue 等
3>Connection:publisher/consumer 和 broker 之间的 TCP 连接
4>Channel:如果每一次访问 RabbitMQ 都建立一个 Connection,在消息量大的时候建立 TCP Connection的开销将是巨大的,效率也较低。Channel 是在 connection 内部建立的逻辑连接,如果应用程序支持多线程,通常每个thread创建单独的 channel 进行通讯,AMQP method 包含了channel id 帮助客户端和message broker 识别 channel,所以 channel 之间是完全隔离的。Channel 作为轻量级的 Connection 极大减少了操作系统建立 TCP connection 的开销
5>Exchange:message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发消息到queue 中去。常用的类型有:direct (point-to-point), topic (publish-subscribe) and fanout (multicast)
6>Queue:消息最终被送到这里等待 consumer 取走
7>Binding:exchange 和 queue 之间的虚拟连接,binding 中可以包含 routing key。Binding 信息被保存到 exchange 中的查询表中,用于 message 的分发依据
RabbitMQ 提供了 6 种工作模式:简单模式、work queues、Publish/Subscribe 发布与订阅模式、Routing 路由模式、Topics 主题模式、RPC 远程调用模式(远程调用,使用的比较少)。
官网对应模式介绍:https://www.rabbitmq.com/getstarted.html
工作模式总结:
1>简单模式:一个生产者、一个消费者,不需要设置交换机(使用默认的交换机);
2>工作队列模式 Work Queue: 一个生产者、多个消费者(竞争关系),不需要设置交换机(使用默认的交换机)。
3>发布订阅模式 Publish/subscribe:需要设置类型为 fanout 的交换机,并且交换机和队列进行绑定,当发送消息到交换机后,交换机会将消息发送到绑定的队列。
4>路由模式 Routing:需要设置类型为 direct 的交换机,交换机和队列进行绑定,并且指定 routing key,当发送消息到交换机后,交换机会根据 routing key 将消息发送到对应的队列。
5>通配符模式 Topic:需要设置类型为 topic 的交换机,交换机和队列进行绑定,并且指定通配符方式的 routing key,当发送消息到交换机后,交换机会根据 routing key 将消息发送到对应的队列。
Topic 类型与 Direct 相比,都是可以根据 RoutingKey 把消息路由到不同的队列。但是 Topic 类型的队列在绑定 Routing key 的时候使用通配符(通配符规则:# 匹配一个或多个词,* 匹配不多不少恰好1个词,例如:key.# 能够匹配 key.msg.a 或者 key.msg,key.* 只能匹配 key.msg)。
1> pom.xml
<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>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.6.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.6.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.73</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test</artifactId>
<scope>test</scope>
</dependency>
2>application.properties
server.port=8888
#基本配置
spring.rabbitmq.port=5672
spring.rabbitmq.username=root
spring.rabbitmq.password=root
spring.rabbitmq.addresses=localhost
spring.rabbitmq.virtual-host=/
#消息的可靠性投递
#1.消息发送方消息确认参数
#springboot版本为2.1.4的时候才有这个属性
#spring.rabbitmq.publisher-confirms=true
spring.rabbitmq.publisher-returns=true
spring.rabbitmq.publisher-confirm-type=correlated
#2.消费者消息确认参数
spring.rabbitmq.listener.simple.acknowledge-mode=manual
#并行消费者数量
spring.rabbitmq.listener.simple.concurrency=3
#最大并行消费者数量
spring.rabbitmq.listener.simple.max-concurrency=5
#消费端限流
spring.rabbitmq.listener.simple.prefetch=100
在使用RabbitMQ的时候,为了防止出现消息丢失或者投递失败场景,RabbitMQ提供了两种方式用来控制消息的投递可靠性模式:
1>confirm确认模式
2>return退回模式
rabbitmq 整个消息投递的路径为:producer—>exchange—>queue—>consumer
消息从 producer 到 exchange 则会返回一个 confirmCallback 。
消息从 exchange–>queue 投递失败则会返回一个 returnCallback 。
#springboot版本为2.1.4的时候才有这个属性
#spring.rabbitmq.publisher-confirms=true
spring.rabbitmq.publisher-returns=true
spring.rabbitmq.publisher-confirm-type=correlated
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class RabbitMqConfig {
@Resource
private RabbitTemplate rabbitTemplate;
/**
* 生产方的消息到达确认机制
* ConfirmCallback 不管消息有没有正确到达exchange,都会被触发
* --如果到达exchange,则confirm回调,ack=true
* --如果未道道exchange,则confirm回调,ack=false
* ReturnCallback 消息未正确到达队列时触发
* --如果到达queue,则不会触发回调
* --如果未到达queue,则触发回调
*
* @return
*/
@PostConstruct
public RabbitTemplate initRabbitTemplate() {
rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
**rabbitTemplate.setMandatory(Boolean.TRUE); // 是否允许执行returnCallback方法**
// 不论消息是否正确到达交换机,都会触发回调
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
/**
* @param correlationData 消息
* @param ack 是否到达交换机的确认结果 true到达交换机 false未到达交换机
* @param cause 未到达交换机的原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
// CorrelationData springboot提供的参数,不是必须的。失败后返回的一个信息,一般就可以将失败的订单ID返回
// 我们可以在发送消息的时候附带一个CorrelationData参数 这个对象可以设置一个id,可以是你的业务id 方便进行对应的操作
// CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
// rabbitTemplate.convertAndSend("directExchange", "direct.key123123", "hello",correlationData);
//System.out.println("confirm方法被执行了...."+correlationData.getId());
//ack 为 true表示 消息已经到达交换机
if (ack) {
//接收成功
System.out.println("接收成功消息" + cause);
} else {
//接收失败
System.out.println("接收失败消息" + cause);
//做一些处理,让消息再次发送。
}
}
});
**// 只有消息未正确到达队列时,才会回调**
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
/**
* @param message 消息
* @param replyCode 失败码
* @param replyText 失败原因
* @param exchange 使用的交换机
* @param routingKey 使用的路由键
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("return 执行了....");
System.out.println("message:" + message);
System.out.println("replyCode:" + replyCode);
System.out.println("replyText:" + replyText);
System.out.println("exchange:" + exchange);
System.out.println("routingKey:" + routingKey);
}
});
return rabbitTemplate;
}
//声明队列
@Bean
public Queue topicMqQ1() {
return new Queue("topic_mq_q1");
}
//声明exchange
@Bean
public TopicExchange setTopicMqExchange() {
return new TopicExchange("topicMqExchange");
}
//声明binding,需要声明一个routingKey
@Bean
public Binding bindTopicMqQ1() {
return BindingBuilder.bind(topicMqQ1()).to(setTopicMqExchange()).with("topic_mq_q1_msg");
}
}
@RabbitListener(queues = "topic_mq_q1")
public void topicReceiveMqq1Msg1(Channel channel, @Payload String message, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws Exception {
try {
// 业务代码
System.out.println("Topic模式 topic_mq_q1 received message1 : " +message);
} finally {
channel.basicAck(deliveryTag,false);
}
}
@Test
public void test() {
String str1 = "I am the queen of computers";
rabbitTemplate.convertAndSend("topicMqExchange","topic_mq_q1_msg",str1);
}
消息的可靠性投递(消息发送方):
1> producer -> exchange
注:在springboot2.2.0.RELEASE 版本之前使用的是spring.rabbitmq.publisher-confirms来配置消息发送到交换器之后是否触发回调方法,但是在2.2.0及之后该属性不再使用,使用spring.rabbitmq.publisher-confirm-type属性配置代替。
1-1>设置ConnectionFactory的publisher-confirms=“true” 或 spring.rabbitmq.publisher-confirm-type=correlated 开启确认模式;
1-2>使用rabbitTemplate.setConfirmCallback设置回调函数。当消息发送到exchange后回调confirm方法。在方法中判断ack,如果为true,则发送成功,如果为false,则发送失败,需要处理。
2> exchange -> queue
2-1>设置ConnectionFactory的publisher-returns=“true” 开启 退回模式;
2-2>设置rabbitTemplate.setMandatory(true);
2-3>使用rabbitTemplate.setReturnCallback设置退回函数,当消息从exchange路由到queue失败后,如果设置了rabbitTemplate.setMandatory(true)参数,则会将消息退回给producer。并执行回调函数returnedMessage。
ack指Acknowledge,表示消费端收到消息后的确认方式。
有三种确认方式:
自动确认:acknowledge=“none”
手动确认:acknowledge=“manual”
根据异常情况确认:acknowledge=“auto”
其中自动确认是指,当消息一旦被Consumer接收到,则自动进行确认收到,并将相应消息从 RabbitMQ 的消息缓存中移除。但是在实际业务处理中,很可能出现消息虽然接收到了但是业务处理出现了异常,但是此时该消息已经被自动确认了,就会出现消息丢失的情况。如果设置了手动确认方式,则需要在业务处理成功后,调用channel.basicAck(),手动签收,如果出现异常,则调用channel.basicNack()方法,让其自动重新发送消息。
// 手动确认
channel.basicAck(deliveryTag,false);
// 当channel.basicNack 第三个参数设为true时,消息签收失败会继续进入消息队列等待消费
// 当channel.basicNack 第三个参数设为false时,消息签收失败,此时消息进入死信队列,完成消费
channel.basicNack(deliveryTag,false,false);
spring.rabbitmq.listener.simple.acknowledge-mode=manual
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class RabbitMqConfig {
//声明队列
@Bean
public Queue topicMqQ1() {
return new Queue("topic_mq_q1");
}
//声明exchange
@Bean
public TopicExchange setTopicMqExchange() {
return new TopicExchange("topicMqExchange");
}
//声明binding,需要声明一个routingKey
@Bean
public Binding bindTopicMqQ1() {
return BindingBuilder.bind(topicMqQ1()).to(setTopicMqExchange()).with("topic_mq_q1_msg");
}
}
@RabbitListener(queues="topic_mq_q1")
public void topicReceiveMqq1Msg1(Channel channel, @Payload String message, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws Exception {
System.out.println("Topic模式 topic_mq_q1 received message : " +message);
try {
// 业务代码
} finally {
channel.basicAck(deliveryTag,false);
}
}
@Test
public void test() {
String str1 = "I am the queen of computers";
rabbitTemplate.convertAndSend("topicMqExchange","topic_mq_q1_msg",str1);
}
运行结果为:
消息的可靠性投递(消费端):
1>在rabbit:listener-container标签中设置acknowledge属性(spring.rabbitmq.listener.simple.acknowledge-mode),设置ack方式 none(自动确认)、manual(手动确认);
2>如果在消费端没有出现异常,则调用channel.basicAck(deliveryTag,false);方法确认签收消息;如果出现异常,则在catch中调用 basicNack或 basicReject,拒绝消息,让MQ重新发送消息。
当RabbitMQ服务器积压了有上万条未处理的消息,我们随便打开一个消费者客户端,会出现这样情况: 巨量的消息瞬间全部推送过来,但是我们单个客户端无法同时处理这么多数据。当数据量特别大的时候,我们对消息发送端限流肯定是不科学的,因为有时候并发量就是特别大,有时候并发量又特别少,我们无法约束发送端,这是用户的行为。所以我们应该对消费端进行限流,用于保持消费端的稳定,当消息数量激增的时候很有可能造成资源耗尽,以及影响服务的性能,导致系统的卡顿甚至直接崩溃。
spring.rabbitmq.listener.simple.concurrency=3
spring.rabbitmq.listener.simple.prefetch=100
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class RabbitMqConfig {
//声明队列
@Bean
public Queue topicMqQ1() {
return new Queue("topic_mq_q1");
}
//声明exchange
@Bean
public TopicExchange setTopicMqExchange() {
return new TopicExchange("topicMqExchange");
}
//声明binding,需要声明一个routingKey
@Bean
public Binding bindTopicMqQ1() {
return BindingBuilder.bind(topicMqQ1()).to(setTopicMqExchange()).with("topic_mq_q1_msg");
}
}
@RabbitListener(queues="topic_mq_q1")
public void topicReceiveMqq1Msg(Channel channel, @Payload String message, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws IOException {
System.out.println(deliveryTag + "-" +message);
channel.basicAck(deliveryTag,false);
}
@Test
public void testQos() throws InterruptedException {
for (int i = 0; i < 10; i++) {
rabbitTemplate.convertAndSend("topicMqExchange","topic_mq_q1_msg","test" + i);
}
}
@Test
public void testQos() throws InterruptedException {
for (int i = 0; i < 500; i++) {
rabbitTemplate.convertAndSend("topicMqExchange","topic_mq_q1_msg","test" + i);
}
}
项目中大部分使用@RabbitmqListener注解的方式处理业务代码中MQ的消费,这个注解用于监听指定的队列,如果containerFactory未指定,默认使用SimpleRabbitListenerContainerFactory实例对象创建一个消息监听容器(SimpleMessageListenerContainer)。默认情况下,rabbitmq的消费者为单线程串行消费,这也是队列的特性。从SimpleMessageListenerContainer的源码可以看到设置并发消费属性concurrentConsumers=1,从字面意义也可以分析出该字段是设置并发消费者的数量,默认为一个监听器设置一个消费者。
private volatile int concurrentConsumers = 1;
rabbitmq容器启动的时候根据设置的concurrentConsumers创建N个BlockingQueueConsumer(N个消费者队列)
protected int initializeConsumers() {
int count = 0;
synchronized(this.consumersMonitor) {
if (this.consumers == null) {
this.cancellationLock.reset();
this.consumers = new HashSet(this.concurrentConsumers);
for(int i = 0; i < this.concurrentConsumers; ++i) {
BlockingQueueConsumer consumer = this.createBlockingQueueConsumer();
this.consumers.add(consumer);
++count;
}
}
return count;
}
}
另外它继承的抽象类AbstractMessageListenerContainer的构造函数,代码中prefetchCount为设置并发消费的另一个关键属性,prefetchCount指一个消费者每次一次性从broker里面取出的待消费的消息个数,默认值prefetchCount=250。
public AbstractMessageListenerContainer() {
this.proxy = this.delegate;
this.shutdownTimeout = 5000L;
this.transactionAttribute = new DefaultTransactionAttribute();
this.taskExecutor = new SimpleAsyncTaskExecutor();
this.recoveryBackOff = new FixedBackOff(5000L, 9223372036854775807L);
this.messagePropertiesConverter = new DefaultMessagePropertiesConverter();
this.missingQueuesFatal = true;
this.possibleAuthenticationFailureFatal = true;
this.autoDeclare = true;
this.mismatchedQueuesFatal = false;
this.failedDeclarationRetryInterval = 5000L;
this.autoStartup = true;
this.phase = 2147483647;
this.active = false;
this.running = false;
this.lifecycleMonitor = new Object();
this.queueNames = new CopyOnWriteArrayList();
this.errorHandler = new ConditionalRejectingErrorHandler();
this.exposeListenerChannel = true;
this.acknowledgeMode = AcknowledgeMode.AUTO;
this.deBatchingEnabled = true;
this.adviceChain = new Advice[0];
this.defaultRequeueRejected = true;
**this.prefetchCount = 250;**
this.lastReceive = System.currentTimeMillis();
this.statefulRetryFatalWithNullMessageId = true;
this.exclusiveConsumerExceptionLogger = new AbstractMessageListenerContainer.DefaultExclusiveConsumerLogger();
this.lookupKeyQualifier = "";
this.forceCloseChannel = true;
}
上面我们已经根据concurrentConsumer的值设置了N个消费者队列,从下面代码中最后一行可以看出消费者队列中维护了一个阻塞队列,其中阻塞队列的大小就由prefetchCount决定。
public BlockingQueueConsumer(ConnectionFactory connectionFactory, MessagePropertiesConverter messagePropertiesConverter, ActiveObjectCounter<BlockingQueueConsumer> activeObjectCounter, AcknowledgeMode acknowledgeMode, boolean transactional, int prefetchCount, boolean defaultRequeueRejected, Map<String, Object> consumerArgs, boolean noLocal, boolean exclusive, String... queues) {
this.cancelled = new AtomicBoolean(false);
this.consumerArgs = new HashMap();
this.deliveryTags = new LinkedHashSet();
this.consumerTags = new ConcurrentHashMap();
this.missingQueues = Collections.synchronizedSet(new HashSet());
this.retryDeclarationInterval = 60000L;
this.failedDeclarationRetryInterval = 5000L;
this.declarationRetries = 3;
this.connectionFactory = connectionFactory;
this.messagePropertiesConverter = messagePropertiesConverter;
this.activeObjectCounter = activeObjectCounter;
this.acknowledgeMode = acknowledgeMode;
this.transactional = transactional;
this.prefetchCount = prefetchCount;
this.defaultRequeueRejected = defaultRequeueRejected;
if (consumerArgs != null && consumerArgs.size() > 0) {
this.consumerArgs.putAll(consumerArgs);
}
this.noLocal = noLocal;
this.exclusive = exclusive;
this.queues = (String[])Arrays.copyOf(queues, queues.length);
this.queue = new LinkedBlockingQueue(prefetchCount);
}
根据队列的特性可知,如果阻塞队列中一个消息阻塞了,那么所有消息将会被阻塞,如果使用默认设置,concurrentConsumer=1,prefetchCount=250,那么当消费者队列中有一个消息由于某种原因阻塞了,那么该消息的后续消息同样不能被消费。为了防止这种情况的发生,我们可以增大concurrentConsumer的设置,使多个消费者可以并发消费。而prefetchCount该如何设置呢?假设conrrentConsumer=2,prefetchCount采用默认值,rabbitmq容器会初始化两个并发的消费者,每个消费者的阻塞队列大小为250,rabbitmq的机制是将消息投递给consumer1,先为consumer1投递满250个message,再往consumer2投递250个message,如果consumer1的message一直小于250个,consumer2一直处于空闲状态,那么并发消费退化为单消费者。
关于concurrentConsumer的设置有两种方式,一种是单个固定的值,如concurrentConsumer=4,另一种是concurrentConsumer=1-4。
public void setConcurrency(String concurrency) {
try {
int separatorIndex = concurrency.indexOf(45);
if (separatorIndex != -1) {
this.setConcurrentConsumers(Integer.parseInt(concurrency.substring(0, separatorIndex)));
this.setMaxConcurrentConsumers(Integer.parseInt(concurrency.substring(separatorIndex + 1, concurrency.length())));
} else {
this.setConcurrentConsumers(Integer.parseInt(concurrency));
}
} catch (NumberFormatException var3) {
throw new IllegalArgumentException("Invalid concurrency value [" + concurrency + "]: only single fixed integer (e.g. \"5\") and minimum-maximum combo (e.g. \"3-5\") supported.");
}
}
concurrency即为我们设置的参数,45为’-’的ascii码,容器首先设置了一个并发消费者,然后设置了最大并发消费者。maxConcurrentConsumer用于处理在极端情况下,可以实例化的最大的消费者数量。可以对比理解成线程池的核心线程数与最大线程数,在每次消费之初都会判断maxConcurrentConsumers是否为空,如果非空会判断是否对消费者进行弹性扩容,其中consecutiveMessages与consecutiveIdles变量控制是需要新增/减少消费者的标志位,对应的参考值分别为consecutiveActiveTrigger和consecutiveIdleTrigger,两个变量的默认值为10。
if (consecutiveMessages++ > SimpleMessageListenerContainer.this.consecutiveActiveTrigger) {
SimpleMessageListenerContainer.this.considerAddingAConsumer();
consecutiveMessages = 0;
}
if (consecutiveIdles++ > SimpleMessageListenerContainer.this.consecutiveIdleTrigger) {
SimpleMessageListenerContainer.this.considerStoppingAConsumer(this.consumer);
consecutiveIdles = 0;
}
当单个消费者连续接受的消息数量达到10个的时候,开始调用considerAddingAConsumer,判断时间是否满足要求对并发消费者进行扩容,反之就是减少消费者数量。
private void considerAddingAConsumer() {
synchronized(this.consumersMonitor) {
if (this.consumers != null && this.maxConcurrentConsumers != null && this.consumers.size() < this.maxConcurrentConsumers) {
long now = System.currentTimeMillis();
if (this.lastConsumerStarted + this.startConsumerMinInterval < now) {
this.addAndStartConsumers(1);
this.lastConsumerStarted = now;
}
}
}
}
RabbitMQ并发消费的两个参数prefetchCount(spring.rabbitmq.listener.simple.prefetch)和concurrentConsumers(spring.rabbitmq.listener.simple.concurrency):
concurrentConsumers是设置并发消费者的个数,可以进行初始化-最大值动态调整,并发消费者可以提高消息的消费能力,防止消息的堆积。
prefechCount是每个消费者一次性从broker中取出的消息个数,提高这个参数并不能对消息实现并发消费,仅仅是减少了网络传输的时间。
注:1>在rabbit:listener-container 中配置 prefetch属性设置消费端一次拉取多少消息
2>消费端的确认模式一定为手动确认。acknowledge=“manual”
TTL(Time To Live,生存时间),当消息到达存活时间后,还没有被消费,会被自动清除。
RabbitMQ可以对消息设置过期时间,也可以对整个队列(Queue)设置过期时间。
@Bean("topicMqTtlQ1")
public Queue topicMqTtlQ1() {
return QueueBuilder.durable("topic_mq_ttl_q1").ttl(10000).build();
}
@Bean("topicMqTtlExchange")
public TopicExchange setTopicMqTtlExchange() {
return new TopicExchange("topicMqTtlExchange");
}
@Bean
public Binding bindTopicMqTtlQ1(@Qualifier("topicMqTtlExchange") TopicExchange topicExchange,@Qualifier("topicMqTtlQ1") Queue queue) {
return BindingBuilder.bind(queue).to(topicExchange).with("topic_mq_ttl_q1_msg");
}
无,10s后队列中的数据会自动消失
@Test
public void testTtl() {
rabbitTemplate.convertAndSend("topicMqTtlExchange","topic_mq_ttl_q1_msg","test");
}
1>设置队列过期时间使用参数:x-message-ttl,单位:ms(毫秒),会对整个队列消息统一过期。
2>设置消息过期时间使用参数:expiration。单位:ms(毫秒),当该消息在队列头部时(消费时),会单独判断这一消息是否过期。
如果两者都进行了设置,以时间短的为准。
@Test
public void testMsgTtl() throws UnsupportedEncodingException {
String message = "I am the queen of computers";
//设置部分请求参数
MessageProperties messageProperties = new MessageProperties();
messageProperties.setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN);
messageProperties.setExpiration("1000");
//发消息
rabbitTemplate.send("topicMqExchange","topic_mq_q1_msg",new Message(message.getBytes(StandardCharsets.UTF_8),messageProperties));
}
1>下单后,30分钟未支付,取消订单,回滚库存;订单成功支付则什么都不做。
2>新用户注册成功7天后,发送短信问候。
1>application.properties
server.port=8888
spring.rabbitmq.port=5672
spring.rabbitmq.username=root
spring.rabbitmq.password=root
spring.rabbitmq.addresses=localhost
spring.rabbitmq.virtual-host=/
spring.rabbitmq.publisher-confirm-type=CORRELATED
spring.rabbitmq.publisher-returns=true
spring.rabbitmq.listener.simple.acknowledge-mode=manual
2>RabbitMqConfig
package com.practice.springboot.config;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class RabbitMqConfig {
@Resource
private RabbitTemplate rabbitTemplate;
@Bean
public Queue topicMqQ2() {
Map<String, Object> arguments = new HashMap<>();
arguments.put("x-dead-letter-exchange","topicMqDelayExchange");
arguments.put("x-dead-letter-routing-key","topic_mq_delay_q2_msg");
arguments.put("x-message-ttl",10000);
return new Queue("topic_mq_q2",true,false,false,arguments);
}
@Bean
public Queue topicMqDelayQ2() {
return new Queue("topic_mq_delay_q2");
}
//声明exchange
@Bean
public TopicExchange setTopicMqExchange() {
return new TopicExchange("topicMqExchange");
}
@Bean
public TopicExchange setTopicMqDelayExchange() {
return new TopicExchange("topicMqDelayExchange");
}
//声明binding,需要声明一个routingKey
@Bean
public Binding bindTopicMqQ2() {
return BindingBuilder.bind(topicMqQ2()).to(setTopicMqExchange()).with("topic_mq_q2_msg");
}
@Bean
public Binding bindTopicMqDelayQ2() {
return BindingBuilder.bind(topicMqDelayQ2()).to(setTopicMqDelayExchange()).with("topic_mq_delay_q2_msg");
}
}
@RabbitListener(queues="topic_mq_delay_q2")
public void topicReceiveMqDelayQ2Msg(Channel channel, @Payload String message, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws IOException {
try {
// 业务代码
System.out.println("Topic模式 topic_mq_delay_q2 received message : " +message);
} finally {
channel.basicAck(deliveryTag,false);
}
}
package com.practice;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import javax.annotation.Resource;
@SpringBootTest
@RunWith(SpringRunner.class)
public class ProducerTest {
@Resource
private RabbitTemplate rabbitTemplate;
/*
* 测试延时消息
* */
@Test
public void testDelay() throws InterruptedException {
//1.发送消息
rabbitTemplate.convertAndSend("topicMqExchange","topic_mq_q2_msg","test");
//2.打印倒计时10秒
for (int i = 10; i > 0 ; i--) {
System.out.println(i+"...");
Thread.sleep(1000);
}
}
}
1> 延迟队列 指消息进入队列后,可以被延迟一定时间,再进行消费。
2>RabbitMq虽没有提供延迟队列功能,但是可以使用 : TTL + DLX 来达到延迟队列效果。
死信,在官网中对应的单词为“Dead Letter”,“死信”是RabbitMQ中的一种消息机制,当你在消费消息时,如果队列里的消息出现以下情况:
1>消息被否定确认,使用 channel.basicNack 或 channel.basicReject ,并且此时requeue 属性被设置为false。
2>消息在队列的存活时间超过设置的生存时间(TTL)时间。
3>消息队列的消息数量已经超过最大队列长度。
那么该消息将成为“死信”。
“死信”消息会被RabbitMQ进行特殊处理,如果配置了死信队列信息,那么该消息将会被丢进死信队列中,如果没有配置,则该消息将会被丢弃。
死信队列的配置:
1>配置业务队列,绑定到业务交换机上
2>为业务队列配置死信交换机和路由key(x-dead-letter-exchange 和 x-dead-letter-routing-key)
3>为死信交换机配置死信队列
基础队列消费失败后,进行重试队列,重试三次后进入死信队列
package com.practice.springboot.config;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
/*
Topics模式 交换机类型 topic
* */
@Configuration
public class RabbitMqConfig {
@Resource
private RabbitTemplate rabbitTemplate;
//声明队列
@Bean
public Queue topicMqQ1() {
return new Queue("topic_mq_q1");
}
@Bean
public Queue topicMqRetryQ1() {
// x-dead-letter-exchange指定重试时将消息重发给哪一个转发器、x-message-ttl消息到达重试队列后,多长时间后重发
Map<String, Object> arguments = new HashMap<>();
arguments.put("x-dead-letter-exchange","topicMqDelayExchange");
arguments.put("x-dead-letter-routing-key","topic_mq_delay_q1_msg");
arguments.put("x-message-ttl",10000);
return new Queue("topic_mq_retry_q1",true,false,false,arguments);
//return QueueBuilder.durable("topic_mq_retry_q1").deadLetterExchange("directDelayExchange").deadLetterRoutingKey("delay.china.changsha").ttl(10000).build();
}
@Bean
public Queue topicMqDelayQ1() {
return new Queue("topic_mq_delay_q1");
}
//声明exchange
@Bean
public TopicExchange setTopicMqExchange() {
return new TopicExchange("topicMqExchange");
}
@Bean
public TopicExchange setTopicMqRetryExchange() {
return new TopicExchange("topicMqRetryExchange");
}
@Bean
public TopicExchange setTopicMqDelayExchange() {
return new TopicExchange("topicMqDelayExchange");
}
//声明binding,需要声明一个routingKey
@Bean
public Binding bindTopicMqQ1() {
return BindingBuilder.bind(topicMqQ1()).to(setTopicMqExchange()).with("topic_mq_q1_msg");
}
@Bean
public Binding bindTopicMqRetryQ1() {
return BindingBuilder.bind(topicMqRetryQ1()).to(setTopicMqRetryExchange()).with("topic_mq_retry_q1_msg");
}
@Bean
public Binding bindTopicMqDelayQ1() {
return BindingBuilder.bind(topicMqDelayQ1()).to(setTopicMqDelayExchange()).with("topic_mq_delay_q1_msg");
}
}
@RabbitListener(queues="topic_mq_q1")
public void topicReceiveMqq1Msg1(Channel channel, @Payload String message, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws Exception {
// MessagingMessageListenerAdapter.invokeHandlerAndProcessResult
// Message message -> @Payload String message, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag
// @Headers Map messageMap
System.out.println("Topic模式 topic_mq_q1 received message : " +message);
try {
// 业务代码
businessHandle();
} catch (Exception e) {
MessageVo vo = new MessageVo(message,1);
retryHandle(vo,"topicMqRetryExchange","topic_mq_retry_q1_msg");
} finally {
channel.basicAck(deliveryTag,false);
}
}
@RabbitListener(queues="topic_mq_retry_q1")
public void topicReceiveMqRetryQ1Msg(Channel channel, @Payload String message, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws Exception {
MessageVo vo = JSON.parseObject(message, MessageVo.class);
if (vo.getRetryCount() > 3) {
// 超过重试次数则不重回队列
// 当channel.basicNack 第三个参数设为true时,消息签收失败会继续进入消息队列等待消费
// 当channel.basicNack 第三个参数设为false时,消息签收失败,此时消息进入死信队列,完成消费
channel.basicNack(deliveryTag,false,false);
System.out.println("超出重试上限,手动进入死信队列");
return;
}
System.out.println("Topic模式 topic_mq_retry_q1 received message : " +message + ",重试第"+ vo.getRetryCount() + "次");
try {
// 执行业务代码
businessHandle();
} catch (Exception e) {
vo.setRetryCount(vo.getRetryCount()+1);
retryHandle(vo,"topicMqRetryExchange","topic_mq_retry_q1_msg");
} finally {
channel.basicAck(deliveryTag,false);
}
}
@RabbitListener(queues="topic_mq_delay_q1")
public void topicReceiveMqDelayQ1Msg(Channel channel, @Payload String message, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws IOException {
System.out.println("Topic模式 topic_mq_delay_q1 received message : " +message);
channel.basicAck(deliveryTag,false);
}
private void businessHandle() throws Exception{
// todo
int i = 1/0;
}
private void retryHandle(MessageVo vo,String retryExchange,String routingKey) throws Exception{
MessageProperties messageProperties = new MessageProperties();
messageProperties.setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN);
//messageProperties.setExpiration("10000"); // 设置过期时间
String message = JSONObject.toJSONString(vo);
rabbitTemplate.send(retryExchange,routingKey,new Message(message.getBytes(StandardCharsets.UTF_8),messageProperties));
}
@Test
public void test() {
String str1 = "I am the queen of computers";
rabbitTemplate.convertAndSend("topicMqExchange","topic_mq_q1_msg",str1);
}
场景1:消费者在消费完一条消息后,向RabbitMQ 发送一个ACK 确认,但是此时网络断开或者其他原因导致RabbitMQ 没有收到这个ACK,那么RabbitMQ 并不会讲该条消息删除,而是重回队列,当客户端重新建立到连接后,消费者还是会再次收到该条消息,这就造成了消息的重复消费。
场景2:消息在发送的时候,同一条消息也可能发送多次。
解决方案
1>生成全局id,存入redis或者数据库,在消费者消费消息之前,查询一下该消息是否有消费过。
2>如果该消息已经消费过,则告诉mq消息已经消费,将该消息丢弃(手动ack)。
3>如果没有消费过,将该消息进行消费并将消费记录写进redis或者数据库中。
注:还有一种方式,数据库操作可以设置唯一键(消息id),防止重复数据的插入,这样插入只会报错而不会插入重复数据。
1>消费者宕机积压
2>消费者消费能力不足积压
3>发送者发流量太大
解决方案:上线更多的消费者,进行正常消费上线专门的队列消费服务,将消息先批量取出来,记录数据库,再慢慢处理
答:不是
@RabbitListener可以用于方法和类上,
1>@RabbitListener标注在方法上,直接监听指定的队列,此时接收的参数需要与发送类型一致
发布端:
@Test
public void testRabbitHandler() {
String str1 = "I am the queen of computers";
rabbitTemplate.convertAndSend("topicMqExchange","topic_mq_q1_msg",str1);
}
消费端:
@RabbitListener(queues = "topic_mq_q1")
public void topicReceiveMqq1Msg1(Channel channel, @Payload String message, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws Exception {
try {
// 业务代码
System.out.println("Topic模式 topic_mq_q1 received message1 : " +message);
} finally {
channel.basicAck(deliveryTag,false);
}
}
运行结果为:
但是,同时有其它类型的参数会出现报错吗?答案是不会,默认将类型转换成了字符串。
@Test
public void testRabbitHandler() {
String[] str = new String[]{"I"," am ","the queen of computers......"};
rabbitTemplate.convertAndSend("topicMqExchange","topic_mq_q1_msg",str);
String str1 = "I am the queen of computers";
rabbitTemplate.convertAndSend("topicMqExchange","topic_mq_q1_msg",str1);
char[] str2 = new char[]{'q','u','e','e','n'};
rabbitTemplate.convertAndSend("topicMqExchange","topic_mq_q1_msg",str2);
}
运行结果为:
2>@RabbitListener 可以标注在类上面,需配合 @RabbitHandler 注解一起使用
@RabbitListener 标注在类上面表示当有收到消息的时候,就交给 @RabbitHandler 的方法处理,根据接受的参数类型进入具体的方法中。
发布端:
@Test
public void testRabbitHandler() {
String[] str = new String[]{"I"," am ","the queen of computers......"};
rabbitTemplate.convertAndSend("topicMqExchange","topic_mq_q1_msg",str);
String str1 = "I am the queen of computers";
rabbitTemplate.convertAndSend("topicMqExchange","topic_mq_q1_msg",str1);
char[] str2 = new char[]{'q','u','e','e','n'};
rabbitTemplate.convertAndSend("topicMqExchange","topic_mq_q1_msg",str2);
}
消费端:
@Component
@RabbitListener(queues = "topic_mq_q1")
public class TopicConcumerReceiver {
@Autowired
private RabbitTemplate rabbitTemplate;
@RabbitHandler
public void topicReceiveMqq1Msg1(Channel channel, @Payload String message, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws Exception {
try {
// 业务代码
System.out.println("Topic模式 topic_mq_q1 received message1 : " +message);
} finally {
channel.basicAck(deliveryTag,false);
}
}
@RabbitHandler
public void topicReceiveMqq1Msg1(Channel channel, @Payload String[] message, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws Exception {
try {
// 业务代码
System.out.println("Topic模式 topic_mq_q1 received message2 : " + Arrays.toString(message));
} finally {
channel.basicAck(deliveryTag,false);
}
}
}
运行结果为:
队列中有一条消息并未消费掉,是因为没有对应的char[]类型的消息体进行消费。
重新运行单元测试的方法,会存在报错信息:该报错指的是消费队列中的那一条消息的时候找不到对应的消费者,此时队列中会出现两条未被消费的消息。
为什么要顺序消费?
保证消息的顺序消费是生产业务场景下经常面临的挑战,例如电商的下单逻辑,在用户下单之后,会发送创建订单和扣减库存的消息,我们需要保证扣减库存在创建订单之后执行。
1>处理业务逻辑后,向MQ发送一条消息,再由消费者从 MQ 中获取 消息落盘到MySQL 中。
2>在这个过程中,可能会有增删改的操作,比如执行顺序是增加、修改、删除。
3>消费者可能换了顺序给执行成删除、修改、增加,所以我们要保证消息的顺序消费。
为什么会不按顺序消费?
对于 RabbitMQ 来说,导致上面顺序错乱的原因通常是消费者是集群部署,不同的消费者消费到了同一订单的不同的消息。
1>如消费者1执行了增加,消费者2执行了修改,消费者C执行了删除
2>但是消费者C执行比消费者B快,消费者B又比消费者A快,就会导致消费消息的时候顺序错乱
3>本该顺序是增加、修改、删除,变成了删除、修改、增加.
如何解决?
RabbitMQ 的问题是由于不同的消息都发送到了同一个 queue 中,多个消费者都消费同一个 queue 的消息。
1>我们可以给 RabbitMQ 创建多个 queue,每个消费者固定消费一个 queue 的消息,
1>生产者发送消息的时候,同一个类型的消息发送到同一个 queue 中
1>由于同一个 queue 的消息是一定会保证有序的,那么同一个订单号的消息就只会被一个消费者顺序消费,从而保证了消息的顺序性。
出现场景:
1>消息生产者产生了重复的消息
2>kafka和rocketmq的offset被回调了
3>消息消费者确认失败
4>消息消费者确认时超时了
5>业务系统主动发起重试
不管是由于生产者产生的消息重复,还是由于消费者导致的消息重复,我们都可以在消费者中来进行解决。(消费者在做业务处理时,要做幂等设计)
推荐方法:增加一张消费消息表,使用messageId做唯一索引,在处理业务逻辑之前,先根据messageId查询一下该消息有没有处理过,如果已经处理过了则直接返回成功,如果没有处理过,则继续做业务处理。
数据一致性分为:强一致性、弱一致性和最终一致性
MQ中遵循的是最终一致性,因此在消费者消费失败后才会增加了重试机制。
重试分为同步重试和异步重试,
同步重试:有些消息量比较小的业务场景,可以采用同步重试,在消费消息时如果处理失败,立刻重试3-5次,如果还是失败,则写入到记录表中。但如果消息量比较大,则不建议使用这种方式,因为如果出现网络异常,可能会导致大量的消息不断重试,影响消息读取速度,造成消息堆积。
异步重试:而消息量比较大的业务场景,建议采用异步重试,在消费者处理失败之后,立刻写入重试表,添加一个job专门定时去重试。还有一种做法是,如果消费失败,自己给同一个topic发一条消息,在后面的某个时间点,自己又会消费到那条消息,起到了重试的效果。但是仅适用于对消息顺序要求不高的场景。
为了解决这个问题,可以增加一张消息发送表,当生产者发完消息之后,会往该表中写入一条数据,状态status标记为待确认。如果消费者读取消息之后,则更新该消息的status为已确认。