文章目录
消息中间件之RabbitMQ
1. 消息中间件概述
1.1. 什么是消息中间件
MQ全称为Message Queue,消息队列是应用程序和应用程序之间的通信方法。
-
为什么使用MQ
在项目中,可将一些无需即时返回且耗时的操作提取出来,进行异步处理,而这种异步处理的方式大大的节省了服务器的请求响应时间,从而提高了系统的吞吐量。
-
开发中消息队列通常有如下应用场景:
1、任务异步处理
将不需要同步处理的并且耗时长的操作由消息队列通知消息接收方进行异步处理。提高了应用程序的响应时间。
2、应用程序解耦合
MQ相当于一个中介,生产方通过MQ与消费方交互,它将应用程序进行解耦合。
3、削峰填谷
如订单系统,在下单的时候就会往数据库写数据。但是数据库只能支撑每秒1000左右的并发写入,并发量再高就容易宕机。低峰期的时候并发也就100多个,但是在高峰期时候,并发量会突然激增到5000以上,这个时候数据库肯定卡死了。
消息被MQ保存起来了,然后系统就可以按照自己的消费能力来消费,比如每秒1000个数据,这样慢慢写入数据库,这样就不会卡死数据库了。
但是使用了MQ之后,限制消费消息的速度为1000,但是这样一来,高峰期产生的数据势必会被积压在MQ中,高峰就被“削”掉了。但是因为消息积压,在高峰期过后的一段时间内,消费消息的速度还是会维持在1000QPS,直到消费完积压的消息,这就叫做填谷
1.2. AMQP 和 JMS
MQ是消息通信的模型;实现MQ的大致有两种主流方式:AMQP、JMS。
1.2.1. AMQP
AMQP是一种协议,更准确的说是一种binary wire-level protocol(链接协议)。这是其和JMS的本质差别,AMQP不从API层进行限定,而是直接定义网络交换的数据格式。
1.2.2. JMS
JMS即Java消息服务(JavaMessage Service)应用程序接口,是一个Java平台中关于面向消息中间件(MOM)的API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。
1.2.3. AMQP 与 JMS 区别
- JMS是定义了统一的接口,来对消息操作进行统一;AMQP是通过规定协议来统一数据交互的格式
- JMS限定了必须使用Java语言;AMQP只是协议,不规定实现方式,因此是跨语言的。
- JMS规定了两种消息模式;而AMQP的消息模式更加丰富
1.3. 消息队列产品
市场上常见的消息队列有如下:
- ActiveMQ:基于JMS
- RabbitMQ:基于AMQP协议,erlang语言开发,稳定性好
- RocketMQ:基于JMS,阿里巴巴产品
- Kafka:类似MQ的产品;分布式消息系统,高吞吐量
1.4. RabbitMQ
RabbitMQ官方地址:http://www.rabbitmq.com/
RabbitMQ是由erlang语言开发,基于AMQP(Advanced Message Queue 高级消息队列协议)协议实现的消息队列,它是一种应用程序之间的通信方法,消息队列在分布式系统开发中应用非常广泛。
RabbitMQ提供了6种模式:
- 简单模式
- work模式
- Publish/Subscribe发布与订阅模式
- Routing路由模式
- Topics主题模式
- RPC远程调用模式(远程调用,不太算MQ)
2. 安装及配置RabbitMQ
2.1window安装
2.1.1安装Erlang
- 原因:RabbitMQ服务端代码是使用并发式语言Erlang编写的,安装Rabbit MQ的前提是安装Erlang。
- 下载地址:http://www.erlang.org/downloads
- 安装完事儿后要记得配置一下系统的环境变量。
- 然后双击系统变量path
-
点击“新建”,将%ERLANG_HOME%\bin加入到path中。
-
最后windows键+R键,输入cmd,再输入erl,看到版本号就说明erlang安装成功了。
2.1.2 安装RabbitMQ
- 下载地址:http://www.rabbitmq.com/download.html
- 双击下载后的.exe文件,安装过程与erlang的安装过程相同。
- RabbitMQ安装好后接下来安装RabbitMQ-Plugins。打开命令行cd,输入RabbitMQ的sbin目录。
- 打开sbin目录,双击rabbitmq-server.bat
-
访问http://localhost:15672
-
默认用户名:guest 密码:guest
注意事项:
data目录中文乱码,插件安装目录错误,直接修改对应的bat文件下设置:
set RABBITMQ_BASE=C:\JAVA\rabbitmq\rabbitmq_server-3.8.14\data
3. RabbitMQ入门
3.1基本案例
- 工程依赖
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.6.0</version>
</dependency>
-
创建连接的工具
public class ConnectionUtil { public static Connection getConnection() throws Exception { //创建连接工厂 ConnectionFactory connectionFactory = new ConnectionFactory(); //主机地址;默认为 localhost connectionFactory.setHost("localhost"); //连接端口;默认为 5672 connectionFactory.setPort(5672); //虚拟主机名称;默认为 / connectionFactory.setVirtualHost("/"); //连接用户名;默认为guest connectionFactory.setUsername("guest"); //连接密码;默认为guest connectionFactory.setPassword("guest"); //创建连接 return connectionFactory.newConnection(); } }`
-
创建生产者
public static void main(String[] args) throws Exception { //创建连接 Connection connection = ConnectionUtil.getConnection(); // 创建频道 Channel channel = connection.createChannel(); /** * 声明交换机 * 参数1:交换机名称 * 参数2:交换机类型,fanout、topic、topic、headers */ channel.exchangeDeclare("直连交换机", BuiltinExchangeType.DIRECT); StringBuilder message = new StringBuilder("新增了商品。Topic模式;routing key 为 insert "); for (int i = 0; i < 10; i++) { // 发送信息 message.append(i); channel.basicPublish(EXCHANGE, "", null, message.toString().getBytes()); System.out.println("已发送消息:" + message); } // 关闭资源 channel.close(); connection.close(); }
-
创建消费者
public static void main(String[] args) throws Exception { Connection connection = ConnectionUtil.getConnection(); // 创建频道 Channel channel = connection.createChannel(); //声明交换机 channel.exchangeDeclare("直连交换机", BuiltinExchangeType.DIRECT); // 声明(创建)队列 /** * 参数1:队列名称 * 参数2:是否定义持久化队列 * 参数3:是否独占本次连接 * 参数4:是否在不使用的时候自动删除队列 * 参数5:队列其它参数 */ channel.queueDeclare("queue", true, false, false, null); //队列绑定交换机 channel.queueBind("queue", "直连交换机", ""); //创建消费者;并设置消息处理 DefaultConsumer consumer = new DefaultConsumer(channel) { @Override /** * consumerTag 消息者标签,在channel.basicConsume时候可以指定 * envelope 消息包的内容,可从中获取消息id,消息routingkey,交换机,消息和重传标志(收到消息失败后是否需要重新发送) * properties 属性信息 * body 消息 */ public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { //路由key System.out.println("路由key为:" + envelope.getRoutingKey()); //交换机 System.out.println("交换机为:" + envelope.getExchange()); //消息id System.out.println("消息id为:" + envelope.getDeliveryTag()); //收到的消息 System.out.println("消费者1-接收到的消息为:" + new String(body, StandardCharsets.UTF_8)); } }; //监听消息 /** * 参数1:队列名称 * 参数2:是否自动确认,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复会删除消息,设置为false则需要手动确认 * 参数3:消息接收到后回调 */ channel.basicConsume("queue", true, consumer); }
-
先启动消费者,在启动生产者
发送消息
消费消息
3.2.相关概念介绍
AMQP 一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。
AMQP是一个二进制协议,拥有一些现代化特点:多信道、协商式,异步,安全,扩平台,中立,高效。
RabbitMQ是AMQP协议的Erlang的实现。
概念 | 说明 |
---|---|
连接 Connection | 一个网络连接,比如TCP/IP套接字连接。 |
会话 Session | 端点之间的命名对话。在一个会话上下文中,保证“恰好传递一次”。 |
信道 Channel | 多路复用连接中的一条独立的双向数据流通道。为会话提供物理传输介质。 |
客户端Client | AMQP连接或者会话的发起者。AMQP是非对称的,客户端生产和消费消息,服务器存储和路由这些消息。 |
服务节点Broker | 消息中间件的服务节点;一般情况下可以将一个RabbitMQ Broker看作一台RabbitMQ 服务器。 |
端点 | AMQP对话的任意一方。一个AMQP连接包括两个端点(一个是客户端,一个是服务器)。 |
消费者Consumer | 一个从消息队列里请求消息的客户端程序。 |
生产者Producer | 一个向交换机发布消息的客户端应用程序。 |
3.3RabbitMQ运转流程
生产者发送消息
- 生产者创建连接(Connection),开启一个信道(Channel),连接到RabbitMQ Broker;
- 声明队列并设置属性;如是否排它,是否持久化,是否自动删除;
- 将路由键(空字符串)与队列绑定起来;
- 发送消息至RabbitMQ Broker;
- 关闭信道;
- 关闭连接
消费接收消息
- 消费者创建连接(Connection),开启一个信道(Channel),连接到RabbitMQ Broker
- 向Broker 请求消费相应队列中的消息,设置相应的回调函数;
- 等待Broker回应闭关投递响应队列中的消息,消费者接收消息;
- 确认(ack,自动确认)接收到的消息;
- RabbitMQ从队列中删除相应已经被确认的消息;
- 关闭信道;
- 关闭连接;
4.模式说明
提前说明
Exchange: 交换机是负责接收生产者发送的消息并且处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。
Exchange有常见以下3种类型:
- (Fanout)广播: 将消息交给所有绑定到交换机的队列
- (Direct)定向: 把消息交给符合指定routing key 的队列
- (Topic)通配符: 把消息交给符合routing pattern(路由模式) 的队列
4.1工作队列模式
Work Queues
与入门程序的简单模式
相比,多了一个或一些消费端,多个消费端共同消费同一个队列中的消息。
**应用场景:**对于 任务过重或任务较多情况使用工作队列可以提高任务处理的速度。
**代码:**跟基本案例一样,多创建几个消费者就是了
在一个队列中如果有多个消费者,那么消费者之间对于同一个消息的关系是竞争的关系。
因为使用的默认交换机是Direct定向交换机
4.2Publish/Subscribe发布与订阅模式
发布订阅模式:
- 每个消费者监听自己的队列。
- 生产者将消息发给broker,由交换机将消息转发到绑定此交换机的每个队列,每个绑定交换机的队列都将接收
这种模式使用的是(Fanout)广播交换机:将消息交给所有绑定到交换机的队列
4.3Routing路由模式
路由模式特点:
- 队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key)
- 消息的发送方在 向 Exchange发送消息时,也必须指定消息的 RoutingKey。
- Exchange不再把消息交给每一个绑定的队列,而是根据消息的RoutingKey进行判断,只有队列的Routingkey与消息的 Routingkey完全一致,才会接收到消息
图解:
- P:生产者,向Exchange发送消息,发送消息时,会指定一个routingKey。
- X:Exchange(交换机),接收生产者的消息,然后把消息递交给与routingKey完全匹配的队列
- C1:消费者,其所在队列指定了需要routingKey 为 error 的消息
- C2:消费者,其所在队列指定了需要routingKey为 info、error、warning 的消息
这种模式与 Publish/Subscribe发布与订阅模式 的区别是交换机的类型为:Direct
还有队列绑定交换机的时候需要指定routingKey。
4.4 Topics通配符模式
区别
跟路由模式的区别在于它是使用Topic交换机
Topic类型与Direct相比,都是可以根据RoutingKey把消息路由到不同的队列。
只不过Topic类型Exchange可以让队列在绑定RoutingKey 的时候使用通配符!
使用方式
Routingkey
一般都是有一个或多个单词组成,多个单词之间以.
分割,例如:item.insert
通配符规则:
#
:匹配一个或多个词
*
:匹配不多不少恰好1个词
图解:
- 红色Queue:绑定的是usa.# ,因此凡是以 usa.开头的routingKey 都会被匹配到
- 黄色Queue:绑定的是**#.news** ,因此凡是以 .news结尾的 routingKey 都会被匹配
小结
Topic主题模式可以实现 Publish/Subscribe发布与订阅模式
和 Routing路由模式
的功能;只是Topic在配置routing key 的时候可以使用通配符,显得更加灵活。
4.5. 模式总结
RabbitMQ工作模式:
- 1、简单模式 HelloWorld 一个生产者、一个消费者,不需要设置交换机 (使用默认的交换机)
- 2、工作队列模式 Work Queue 一个生产者、多个消费者 (竞争关系),不需要设置交换机 (使用默认的交换机)
- 3、发布订阅模式 Publish/subscribe 需要设置类型为fanout的交换机,并且交换机和队列进行绑定,当发送消息到交换机后,交换机会将消息发送到绑定的队列
- 4、路由模式 Routing 需要设置类型为direct的交换机,交换机和队列进行绑定,并且指定
routing key
,当发送消息到交换机后,交换机会根据routing key
将消息发送到对应的队列 - 5、通配符模式 Topic 需要设置类型为topic的交换机,交换机和队列进行绑定,并且指定通配符方式的
routing key
,当发送消息到交换机后,交换机会根据routing key
将消息发送到对应的队列
5.SpringBoot整合
-
引入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
-
配置文件
spring.rabbitmq.host=localhost spring.rabbitmq.port=5672 spring.rabbitmq.username=guest spring.rabbitmq.password=guest spring.rabbitmq.virtual-host= spring.rabbitmq.publisher-confirm-type=correlated spring.rabbitmq.publisher-returns=true
-
绑定交换机和队列
@Configuration public class RabbitMQConfig { //交换机名称 public static final String ITEM_TOPIC_EXCHANGE = "item_topic_exchange"; //队列名称 public static final String ITEM_QUEUE = "item_queue"; //声明交换机 @Bean("itemTopicExchange") public Exchange topicExchange(){ return ExchangeBuilder.topicExchange(ITEM_TOPIC_EXCHANGE).durable(true).build(); } //声明队列 @Bean("itemQueue") public Queue itemQueue(){ return QueueBuilder.durable(ITEM_QUEUE).build(); } //绑定队列和交换机 @Bean public Binding itemQueueExchange(@Qualifier("itemQueue") Queue queue, @Qualifier("itemTopicExchange") Exchange exchange){ return BindingBuilder.bind(queue).to(exchange).with("item.#").noargs(); } }
-
发送消息
//注入RabbitMQ的模板 @Autowired private RabbitTemplate rabbitTemplate; /** * 测试 */ @GetMapping("/sendmsg") public String sendMsg(@RequestParam String msg, @RequestParam String key){ /** * 发送消息 * 参数一:交换机名称 * 参数二:路由key * 参数三:发送的消息 */ rabbitTemplate.convertAndSend(RabbitMQConfig.ITEM_TOPIC_EXCHANGE ,key ,msg); //返回消息 return "发送消息成功!"; }
-
创建消费者
@Component public class MyListener { /** * 监听某个队列的消息 * @param message 接收到的消息 */ @RabbitListener(queues = "item_queue") public void myListener1(String message){ System.out.println("消费者接收到的消息为:" + message); } }
-
启动往controller发送信息就行了
6.RabbitMQ高级知识
6.1过期时间
过期时间TTL表示可以对消息设置预期的时间,在这个时间内都可以被消费者接收获取;过了之后消息将自动被删除。RabbitMQ可以对消息和队列设置TTL。目前有两种方法可以设置。
- 第一种方法是通过队列属性设置,队列中所有消息都有相同的过期时间。
- 第二种方法是对消息进行单独设置,每条消息TTL可以不同。
如果上述两种方法同时使用,则消息的过期时间以两者之间TTL较小的那个数值为准。消息在队列的生存时间一旦超过设置的TTL值,就称为deadMessage被投递到死信队列, 消费者将无法再收到该消息。
设置方式
- 设置队列失效时间
@Bean(ITEM_DEAD_QUEUE)
public Queue itemDeadQueue() {
return QueueBuilder.durable(ITEM_DEAD_QUEUE).expires(1000).build();
}
原生的channel声明队列传入参数声明
/**
* 参数1:队列名称
* 参数2:是否定义持久化队列
* 参数3:是否独占本次连接
* 参数4:是否在不使用的时候自动删除队列
* 参数5:队列其它参数
*/
channel.queueDeclare(BaseProducer.QUEUE_2, true, false, false, null);
- 可设置的参数列表
x-message-ttl
发送到队列的消息在丢弃之前可以存活多长时间(毫秒)。x-expires
队列在被自动删除(毫秒)之前可以使用多长时间。x-max-length
队列的最大长度。x-max-length-bytes
队列在开始从头部删除之前可以包含的就绪消息的总体大小。x-dead-letter-exchange
死信交换机,消息过期,被拒绝或者队列过长会被转发到这个交换机x-dead-letter-routing-key
使用死信交换机时需要的routingKeyx-max-priority
队列支持的最大优先级数;如果未设置,队列将不支持消息优先级。x-queue-mode
将队列设置为lazy延迟模式,在磁盘上保留尽可能多的消息以减少内存使用;如果未设置,队列将保留内存缓存以尽快传递消息。参考:https://honeypps.com/mq/rabbitmq-analysis-of-lazy-queue/x-queue-master-locator
将队列设置为主位置模式,确定在节点集群上声明时队列主机所在的规则。
6.2死信队列
DLX,全称为Dead-Letter-Exchange , 可以称之为死信交换机,也有人称之为死信邮箱。当消息在一个队列中变成死信(dead message)之后,它能被重新发送到另一个交换机中,这个交换机就是DLX ,绑定DLX的队列就称之为死信队列。
消息变成死信,可能是由于以下的原因:
- 消息被拒绝
- 消息过期
- 队列达到最大长度
DLX也是一个正常的交换机,和一般的交换机没有区别,它能在任何的队列上被指定,实际上就是设置某一个队列的属性。当这个队列中存在死信时,Rabbitmq就会自动地将这个消息重新发布到设置的DLX上去,进而被路由到另一个队列,即死信队列。
要想使用死信队列,只需要在定义队列的时候设置队列参数 x-dead-letter-exchange
指定交换机即可。
例子:
@Bean(ITEM_QUEUE_2)
public Queue itemQueue2() {
return QueueBuilder.durable(ITEM_QUEUE_2).deadLetterExchange(ITEM_DEAD_EXCHANGE).build();
}
图解:
6.3. 延迟队列
延迟队列存储的对象是对应的延迟消息;所谓“延迟消息” 是指当消息被发送以后,并不想让消费者立刻拿到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费。
在RabbitMQ中延迟队列可以通过 过期时间
+ 死信队列
来实现;
延迟队列的应用场景;如:
- 在电商项目中的支付场景;如果在用户下单之后的几十分钟内没有支付成功;那么这个支付的订单算是支付失败,要进行支付失败的异常处理(将库存加回去),这时候可以通过使用延迟队列来处理
- 在系统中如有需要在指定的某个时间之后执行的任务都可以通过延迟队列处理
6.4. 消息确认机制
确认并且保证消息被送达,提供了两种方式:
发布确认和事务。
(两者不可同时使用)在channel为事务时,不可引入确认模式;
同样channel为确认模式下,不可使用事务。
事务
参考:
https://blog.csdn.net/liubenlong007/article/details/104428297
https://blog.csdn.net/zhanngle/article/details/86267986
消息确认
参考:https://blog.csdn.net/zhangweiwei2020/article/details/107250202/
- 设置配置文件
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=
spring.rabbitmq.publisher-confirm-type=correlated # 设置发送到brokers确认
spring.rabbitmq.publisher-returns=true # 设置发送到queue确认
spring.rabbitmq.listener.simple.acknowledge-mode=manual # 手动确认消费
spring.rabbitmq.listener.simple.retry.enabled=true # 重试
spring.rabbitmq.listener.simple.retry.initial-interval=3000ms #重试时间间隔
spring.rabbitmq.listener.simple.retry.max-attempts=3 #重试次数
spring.rabbitmq.listener.simple.retry.max-interval=15000ms #重试最大时间间隔
spring.rabbitmq.listener.simple.retry.multiplier=2 #倍数
发布确认
确认生产者 producer
将消息发送到 broker
,broker
上的交换机 exchange
再投递给队列 queue
的过程中,消息是否成功投递。
消息从 producer
到 rabbitmq broker
有一个 confirmCallback
确认模式。
消息从 exchange
到 queue
投递失败有一个 returnCallback
退回模式。
我们可以利用这两个Callback
来确保消息的**100%**送达。
- 设置
confirmCallback
实现ConfirmCallback
函数式接口,在rabbitTemplate
发送消息时设置
rabbitTemplate.setConfirmCallback(
(correlationData, ack, cause) -> {
System.out.println("发送成功" + correlationData);
System.out.println(ack);
System.out.println(cause);
});
参数说明:
correlationData
:对象内部只有一个 id
属性,用来表示当前消息的唯一性。
ack
:消息投递到broker
的状态,true
表示成功。
cause
:表示投递失败的原因。
- 设置
returnCallback
实现ReturnCallback
函数式接口,在rabbitTemplate
发送消息时设置
rabbitTemplate.setReturnCallback((mess, replyCode, replyText, exchange, routingKey) -> {
System.out.println("发送失败" + mess);
System.out.println(replyCode);
System.out.println(replyText);
System.out.println(exchange);
System.out.println(routingKey);
});
方法有五个参数message
(消息体)、replyCode
(响应code)、replyText
(响应内容)、exchange
(交换机)、routingKey
(队列)。
消费确认
消息接收确认要比消息发送确认简单一点,因为只有一个消息回执(ack
)的过程。使用@RabbitHandler
注解标注的方法要增加 channel
(信道)、message
两个参数。
@Slf4j
@Component
@RabbitListener(queues = "confirm_test_queue")
public class ReceiverMessage1 {
@RabbitHandler
public void processHandler(String msg, Channel channel, Message message) throws IOException {
try {
log.info("小富收到消息:{}", msg);
//TODO 具体业务
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
if (message.getMessageProperties().getRedelivered()) {
log.error("消息已重复处理失败,拒绝再次接收...");
channel.basicReject(message.getMessageProperties().getDeliveryTag(), false); // 拒绝消息
} else {
log.error("消息即将再次返回队列处理...");
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
}
}
}
}
消费消息有三种回执方法,我们来分析一下每种方法的含义。
basicAck:
表示成功确认,使用此回执方法后,消息会被rabbitmq broker 删除。
void basicAck(long deliveryTag, boolean multiple)
deliveryTag
:表示消息投递序号,每次消费消息或者消息重新投递后,deliveryTag都会增加。手动消息确认模式下,我们可以对指定deliveryTag的消息进行ack、nack、reject等操作。
multiple
:是否批量确认,值为 true 则会一次性 ack所有小于当前消息 deliveryTag 的消息。
举个栗子:
假设我先发送三条消息deliveryTag分别是5、6、7,可它们都没有被确认,当我发第四条消息此时deliveryTag为8,multiple设置为 true,会将5、6、7、8的消息全部进行确认。
basicNack :
表示失败确认,一般在消费消息业务异常时用到此方法,可以将消息重新投递入队列。
void basicNack(long deliveryTag, boolean multiple, boolean requeue)
deliveryTag
:表示消息投递序号。
multiple
:是否批量确认。
requeue
:值为 true 消息将重新入队列。
basicReject:
拒绝消息,与basicNack
区别在于不能进行批量操作,其他用法很相似。
void basicReject(long deliveryTag, boolean requeue)
deliveryTag
:表示消息投递序号。
requeue
:值为 true 消息将重新入队列。
7.应用以及面试题
7.1 消息堆积
当消息生产的速度长时间,远远大于消费的速度时。就会造成消息堆积。
消息堆积的影响
- 可能导致新消息无法进入队列
- 可能导致旧消息无法丢失
- 消息等待消费的时间过长,超出了业务容忍范围。
产生堆积的情况
- 生产者突然大量发布消息
- 消费者消费失败
- 消费者出现性能瓶颈。
- 消费者挂掉
解决办法
参考博客:https://blog.csdn.net/u013825231/article/details/102986563
- 排查消费者的消费性能瓶颈
- 增加消费者的多线程处理
- 部署增加多个消费者
7.2. 消息丢失
在实际的生产环境中有可能出现一条消息因为一些原因丢失,导致消息没有消费成功,从而造成数据不一致等问题,造成严重的影响。
7.2.1. 消息在生产者丢失
场景介绍
消息生产者发送消息成功,但是MQ没有收到该消息,消息在从生产者传输到MQ的过程中丢失,一般是由于网络不稳定的原因。
解决方案
采用RabbitMQ 发送方消息确认机制,当消息成功被MQ接收到时,会给生产者发送一个确认消息,表示接收成功。RabbitMQ 发送方消息确认模式有以下三种:
- 普通确认模式
- 批量确认模式
- 异步监听确认模式
Spring整合RabbitMQ后只使用了异步监听确认模式。
说明
异步监听模式,可以实现边发送消息边进行确认,不影响主线程任务执行。
7.2.2. 消息在RabbitMQ丢失
场景介绍
消息成功发送到MQ,消息还没被消费却在MQ中丢失,比如MQ服务器宕机或者重启会出现这种情况
解决方案
持久化交换机,队列,消息,确保MQ服务器重启时依然能从磁盘恢复对应的交换机,队列和消息。
spring整合后默认开启了交换机,队列,消息的持久化,所以不修改任何设置就可以保证消息不在RabbitMQ丢失。但是为了以防万一,还是可以申明下。
7.2.3. 消息在消费者丢失
场景介绍
消息费者消费消息时,如果设置为自动回复MQ,消息者端收到消息后会自动回复MQ服务器,MQ则会删除该条消息,如果消息已经在MQ被删除但是消费者的业务处理出现异常或者消费者服务宕机,那么就会导致该消息没有处理成功从而导致该条消息丢失。
解决方案
设置为手动回复MQ服务器,当消费者出现异常或者服务宕机时,MQ服务器不会删除该消息,而是会把消息重发给绑定该队列的消费者,如果该队列只绑定了一个消费者,那么该消息会一直保存在MQ服务器,直到消息者能正常消费为止。本解决方案以一个队列绑定多个消费者为例来说明,一般在生产环境上也会让一个队列绑定多个消费者也就是工作队列模式来减轻压力,提高消息处理效率
MQ重发消息场景:
1.消费者未响应ACK,主动关闭频道或者连接
2.消费者未响应ACK,消费者服务挂掉
7.3. 有序消费消息
7.3.1. 场景介绍
场景1
当RabbitMQ采用work Queue模式,此时只会有一个Queue但是会有多个Consumer,同时多个Consumer直接是竞争关系,此时就会出现MQ消息乱序的问题。
场景2
当RabbitMQ采用简单队列模式的时候,如果消费者采用多线程的方式来加速消息的处理,此时也会出现消息乱序的问题。
7.3.2. 场景1解决
7.3.3. 场景2解决
7.4. 重复消费
7.4.1. 场景介绍
为了防止消息在消费者端丢失,会采用手动回复MQ的方式来解决,同时也引出了一个问题,消费者处理消息成功,手动回复MQ时由于网络不稳定,连接断开,导致MQ没有收到消费者回复的消息,那么该条消息还会保存在MQ的消息队列,由于MQ的消息重发机制,会重新把该条消息发给和该队列绑定的消息者处理,这样就会导致消息重复消费。而有些操作是不允许重复消费的,比如下单,减库存,扣款等操作。
MQ重发消息场景:
1.消费者未响应ACK,主动关闭频道或者连接
2.消费者未响应ACK,消费者服务挂掉
7.4.2. 解决方案
如果消费消息的业务是幂等性操作(同一个操作执行多次,结果不变)就算重复消费也没问题,可以不做处理,如果不支持幂等性操作,如:下单,减库存,扣款等,那么可以在消费者端每次消费成功后将该条消息id保存到数据库,每次消费前查询该消息id,如果该条消息id已经存在那么表示已经消费过就不再消费否则就消费。本方案采用redis存储消息id,因为redis是单线程的,并且性能也非常好,提供了很多原子性的命令,本方案使用setnx命令存储消息id
setnx(key,value): # 如果key不存在则插入成功且返回1,如果key存在,则不进行任何操作,返回0