一、 MQ 的基本概念
1.1 MQ概述
MQ全称 Message Queue(消息队列),是在消息的传输过程中保存消息的容器。多用于分布式系统之间进 行通信。
- MQ,消息队列,存储消息的中间件
- 分布式系统通信两种方式:直接远程调用 和 借助第三方 完成间接通信
- 发送方称为生产者,接收方称为消费者
1.2 MQ 的优势和劣势
-
优势:
- 应用解耦 :不同服务之间通过mq通信,相互独立;
- 异步提速 :消息存入队列中可先返回,后异步去消费消息;
- 削峰填谷(使用了 MQ 之后,限制消费消息的速度为1000,这样一来,高峰期产生的数据势必会被积压在 MQ 中,高峰 就被“削”掉了,但是因为消息积压,在高峰期过后的一段时间内,消费消息的速度还是会维持在1000,直 到消费完积压的消息,这就叫做“填谷”。)
-
劣势
- 系统可用性降低:系统引入的外部依赖越多,系统稳定性越差。一旦 MQ 宕机,就会对业务造成影响。如何保证MQ的高可用?
- 系统复杂度提高 :MQ 的加入大大增加了系统的复杂度,以前系统间是同步的远程调用,现在是通过 MQ 进行异步调用。如何 保证消息没有被重复消费?怎么处理消息丢失情况?那么保证消息传递的顺序性?
- 一致性问题:A 系统处理完业务,通过 MQ 给B、C、D三个系统发消息数据,如果 B 系统、C 系统处理成功,D 系统处理 失败。如何保证消息数据处理的一致性?
1.3 使用MQ需要满足的条件
- 生产者不需要从消费者处获得反馈。引入消息队列之前的直接调用,其接口的返回值应该为空,这才让明明下层的动作还没做,上层却当成动作做完了继续往后走,即所谓异步成为了可能。
- 容许短暂的不一致性。
- 确实是用了有效果。即解耦、提速、削峰这些方面的收益,超过加入MQ,管理MQ这些成本。
1.4 RabbitMQ简介
AMQP,即 Advanced Message Queuing Protocol(高级消息队列协议),是一个网络协议,是应用层协议 的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中 间件不同产品,不同的开发语言等条件的限制。2006年,AMQP 规范发布。类比HTTP。
2007年,Rabbit 技术公司基于 AMQP 标准开发的 RabbitMQ 1.0 发布。RabbitMQ 采用 Erlang 语言开发。 Erlang 语言由 Ericson 设计,专门为开发高并发和分布式系统的一种语言,在电信领域使用广泛。
RabbitMQ 基础架构如下图:
- Broker:接收和分发消息的应用,RabbitMQ Server就是 Message Broker
- Virtual host:出于多租户和安全因素设计的,把 AMQP 的基本组件划分到一个虚拟的分组中,类似于网络中的 namespace 概念。当多个不同的用户使用同一个 RabbitMQ server 提供的服务时,可以划分出多个vhost,每个用户在自己的 vhost 创建 exchange/queue 等
- Connection:publisher/consumer 和 broker 之间的 TCP 连接
- Channel:如果每一次访问 RabbitMQ 都建立一个 Connection,在消息量大的时候建立 TCP Connection的开销将是巨大的,效率也较低。Channel 是在connection 内部建立的逻辑连接,如果应用程序支持多线程,通常每个thread创建单独的 channel 进行通讯,AMQP method 包含了channel id 帮助客户端和 message broker 识别 channel,所以 channel 之间是完全隔离的。Channel 作为轻量级的 Connection极大减少了操作系统建立 TCP connection 的开销
- Exchange:message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发消息到 queue 中去。常用的类型有:direct (point-to-point), topic (publish-subscribe) and fanout (multicast)
- Queue:消息最终被送到这里等待 consumer 取走
- Binding:exchange 和 queue 之间的虚拟连接,binding 中可以包含 routing key。Binding 信息被保存 到 exchange 中的查询表中,用于 message 的分发依据
RabbitMQ 提供了 6 种工作模式:简单模式、work queues、Publish/Subscribe 发布与订阅模式、Routing 路由模式、Topics 主题模式、RPC 远程调用模式(远程调用,不太算 MQ)。(https://www.rabbitmq.com/getstarted.html)
1.5 JMS
- JMS 即 Java 消息服务(JavaMessage Service)应用程序接口,是一个 Java 平台中关于面向消息中间件
的API - JMS 是 JavaEE 规范中的一种,类比JDBC
- 很多消息中间件都实现了JMS规范,例如:ActiveMQ。RabbitMQ 官方没有提供 JMS 的实现包,但是开
源社区有
二、RabbitMQ入门程序
2.1 引入依赖
<dependencies>
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.6.0</version>
</dependency>
</dependencies>
2.3 生产者生产消息
public void sendMessage(String message) throws IOException, TimeoutException {
Connection connection = null;
Channel channel = null;
try {
//1.创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
//2. 设置参数
connectionFactory.setHost("172.168.0.100");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/guoVir");
connectionFactory.setUsername("guo");
connectionFactory.setPassword("guo");
//3. 创建连接 Connection
connection = connectionFactory.newConnection();
//4. 创建Channel
channel = connection.createChannel();
//5. 创建队列Queue
/*
queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments)
参数:
1. queue:队列名称
2. durable:是否持久化,当mq重启之后,还在
3. exclusive:
* 是否独占。只能有一个消费者监听这队列
* 当Connection关闭时,是否删除队列
4. autoDelete:是否自动删除。当没有Consumer时,自动删除掉
5. arguments:参数。
*/
//如果没有一个名字叫hello_world的队列,则会创建该队列,如果有则不会创建
channel.queueDeclare("hello_world", true, false, false, null);
//6. 发送消息
/*
basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body)
参数:
1. exchange:交换机名称。简单模式下交换机会使用默认的 ""
2. routingKey:路由名称
3. props:配置信息
4. body:发送消息数据
*/
channel.basicPublish("", "hello_world", null, message.getBytes());
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
} finally {
//7.释放资源
channel.close();
connection.close();
}
}
2.4 消费者消费消息
public void getMessage() {
Connection connection = null;
Channel channel = null;
try {
//1.创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
//2. 设置参数
connectionFactory.setHost("172.168.0.100");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/guoVir");
connectionFactory.setUsername("guo");
connectionFactory.setPassword("guo");
//3. 创建连接 Connection
connection = connectionFactory.newConnection();
//4. 创建Channel
channel = connection.createChannel();
//5. 创建队列Queue
/*
queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments)
参数:
1. queue:队列名称
2. durable:是否持久化,当mq重启之后,还在
3. exclusive:
* 是否独占。只能有一个消费者监听这队列
* 当Connection关闭时,是否删除队列
*
4. autoDelete:是否自动删除。当没有Consumer时,自动删除掉
5. arguments:参数。
*/
//如果没有一个名字叫hello_world的队列,则会创建该队列,如果有则不会创建
channel.queueDeclare("hello_world", true, false, false, null);
/*
6.消费消息
basicConsume(String queue, boolean autoAck, Consumer callback)
参数:
1. queue:队列名称
2. autoAck:是否自动确认
3. callback:回调对象
*/
Consumer defaultConsumer = new DefaultConsumer(channel) {
@Override
/*
回调方法,当收到消息后,会自动执行该方法
1. consumerTag:标识
2. envelope:获取一些信息,交换机,路由key...
3. properties:配置信息
4. body:数据
*/
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("consumerTag:" + consumerTag);
System.out.println("Exchange:" + envelope.getExchange());
System.out.println("RoutingKey:" + envelope.getRoutingKey());
System.out.println("properties:" + properties);
System.out.println("body:" + new String(body));
}
};
//消费消息
channel.basicConsume("hello_world", true, defaultConsumer);
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
上述的入门案例中其实使用的是如下的简单模式:
在上图的模型中,有以下概念:
P:生产者,也就是要发送消息的程序
C:消费者:消息的接收者,会一直等待消息到来
queue:消息队列,图中红色部分。类似一个邮箱,可以缓存消息;生产者向其中投递消息,消费者从其中取出消息
三、RabbitMQ 的工作模式
3.1 Work queues 工作队列模式
Work Queues:与入门程序的简单模式相比,多了一个或一些消费端,多个消费端共同消费同一个队列中的消息。
应用场景:对于任务过重或任务较多情况使用工作队列可以提高任务处理的速度。
1.在一个队列中如果有多个消费者,那么消费者之间对于同一个消息的关系是竞争的关系。
2.Work Queues 对于任务过重或任务较多情况使用工作队列可以提高任务处理的速度。例如:短信服务部署多个, 只需要有一个节点成功发送即可。
3.2 Pub/Sub (Fanout)订阅模式
在订阅模型中,多了一个 Exchange 角色,而且过程略有变化:
- P:生产者,也就是要发送消息的程序,但是不再发送到队列中,而是发给X(交换机)
- C:消费者,消息的接收者,会一直等待消息到来
- Queue:消息队列,接收消息、缓存消息
- Exchange:交换机(X)。一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、 递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。Exchange有常见以下3种类型:
- Fanout:广播,将消息交给所有绑定到交换机的队列
- Direct:定向,把消息交给符合指定routing key 的队列
- Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列
Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与 Exchange 绑定,或者没有符合 路由规则的队列,那么消息会丢失!
3.3 Routing 路由模式
队列与交换机的绑定,不能是任意绑定了,而是要指定一个 RoutingKey(路由key)
消息的发送方在向 Exchange 发送消息时,也必须指定消息的 RoutingKey
Exchange 不再把消息交给每一个绑定的队列,而是根据消息的 Routing Key 进行判断,只有队列的 Routingkey 与消息的 Routing key 完全一致,才会接收到消息
P:生产者,向 Exchange 发送消息,发送消息时,会指定一个routing key
X:Exchange(交换机),接收生产者的消息,然后把消息递交给与 routing key 完全匹配的队列
C1:消费者,其所在队列指定了需要 routing key 为 error 的消息
C2:消费者,其所在队列指定了需要 routing key 为 info、error、warning 的消息
3.4 Topics 通配符模式
- Topic 类型与 Direct 相比,都是可以根据 RoutingKey 把消息路由到不同的队列。只不过 Topic 类型 Exchange 可以让队列在绑定 Routing key 的时候使用通配符!
- Routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert
- 通配符规则:符号 “#” 匹配路由键Routing key的一个或多个词,符号 “**” 匹配路由键Routing key的一个词。“#” 和 “” 都可以代表任意字符或是空,多个字符需要用 “.” 连接 ,例如:item.# 能够匹配 item.insert.abc 或者 item.insert,item. 只能匹配 item.insert
Topic 主题模式可以实现 Pub/Sub 发布与订阅模式和 Routing 路由模式的功能,只是 Topic 在配置routing key 的时候可以使用通配符,显得更加灵活
3.5 工作模式总结
- 简单模式 HelloWorld 一个生产者、一个消费者,不需要设置交换机(使用默认的交换机)
- 工作队列模式 Work Queue 一个生产者、多个消费者(竞争关系),不需要设置交换机(使用默认的交换机)。
- 发布订阅模式 Publish/subscribe 需要设置类型为 fanout 的交换机,并且交换机和队列进行绑定,当发送消息到交换机后,交换机会将消 息发送到绑定的队列。
- 路由模式 Routing 需要设置类型为 direct 的交换机,交换机和队列进行绑定,并且指定 routing key,当发送消息到交换机 后,交换机会根据 routing key 将消息发送到对应的队列。
- 通配符模式 Topic 需要设置类型为 topic 的交换机,交换机和队列进行绑定,并且指定通配符方式的 routing key,当发送 消息到交换机后,交换机会根据 routing key 将消息发送到对应的队列。
四、springboot整合RabbitMQ
4.1 生产者
1.引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2.编写配置
spring:
rabbitmq:
host: 172.168.0.100
port: 5672
username: guo
password: guo
virtual-host: /guoVir
3.定义交换机,队列以及绑定关系的配置类
package com.gou.rabbitmq.config;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author guozhijiang
* @version 1.0
* @description: TODO
* @date 2022/7/22 15:26
*/
@Configuration
//以Direct方式为例
public class DirectRabbitMQConfig {
public static final String EXCHANGE_NAME = "direct_exchange";
public static final String QUEUE_FIRST = "queue_first";
public static final String QUEUE_SECOND = "queue_second";
@Bean
public Exchange directExchange(){
//创建交换机和队列有两种方式:
//1.用ExchangeBuilder.build();
return ExchangeBuilder.directExchange(EXCHANGE_NAME).durable(true).build();
//2.直接new对应的交换机类型 DirectExchange(String name, boolean durable, boolean autoDelete)
//new DirectExchange(EXCHANGE_NAME,true,false);
}
// durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
// exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
// autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。
// return new Queue("TestDirectQueue",true,true,false);
@Bean
public Queue firstQueue(){
//第一种方式
QueueBuilder.durable(QUEUE_FIRST).build();
//第二种方式
return new Queue(QUEUE_FIRST,true,false,false);
}
@Bean
public Queue secondQueue(){
//第一种方式
return QueueBuilder.durable(QUEUE_SECOND).build();
//第二种方式
//return new Queue(QUEUE_FIRST,true,false,false);
}
//绑定==============
//以下是两种不同的绑定方式,常用第一种
//交换机跟firstqueue绑定,指定routingkey为first
@Bean
public Binding bindChangeAndFirstQueue(){
return BindingBuilder.bind(firstQueue()).to(directExchange()).with("first").noargs();
}
//交换机跟secondqueue绑定,指定routingkey为second
@Bean
public Binding bindChangeAndSecondQueue(@Qualifier("secondQueue") Queue queue,
@Qualifier("directExchange") Exchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("second").noargs();
}
}
4.发送消息到队列
rabbitTemplate.convertAndSend(exchange,routingKey,msg);
4.2 消费者
创建监听类监听指定队列
@RabbitListener 注解是指定某方法作为消息消费的方法,例如监听某 Queue 里面的消息,有以下两种常见使用方式
-
@RabbitListener标注在方法上,直接监听指定的队列,此时接收的参数需要与发送市类型一致
@Component public class DirectReceiver { @RabbitListener(queues = "queue_first") public void firstQueue(String msg){ System.out.println("+++"+msg); } @RabbitListener(queues = "queue_second") public void secondQueue(Message msg){ System.out.println("+++"+msg); } }
-
@RabbitListener 可以标注在类上面,需配合 @RabbitHandler 注解一起使用
@RabbitListener 标注在类上面表示当有收到消息的时候,就交给 @RabbitHandler 的方法处理,根据接收的参数类型进入具体的方法中。@Component @RabbitListener(queues = "topic_queue_first") public class TopicFirstReceiver { @RabbitHandler public void getFirst(String msg){ System.out.println(msg); } @RabbitHandler public void getSecond(Map msg){ System.out.println(msg); } }
五、高级特性
5.1 消息的可靠投递
在使用 RabbitMQ 的时候,作为消息发送方希望杜绝任何消息丢失或者投递失败场景。RabbitMQ 为我们提 供了两种方式用来控制消息的投递可靠性模式。
- confirm 确认模式
- return 退回模式
rabbitmq 整个消息投递的路径为: producer—>rabbitmq broker—>exchange—>queue—>consumer
- 消息从 producer 到 exchange 则会返回一个 confirmCallback 。
- 消息从 exchange–>queue 投递失败则会返回一个 returnCallback 。 我们将利用这两个 callback 控制消息的可靠性投递
1.开启confirmcallback和returncallback
spring:
rabbitmq:
host: 172.168.0.100
port: 5672
username: guo
password: guo
virtual-host: /guoVir
##开启发布确认 (三种模式)
# SIMPLE, 同步确认,单次,批量确认 通过invoke调用
# rabbitTemplate.invoke(operations -> {
# rabbitTemplate.convertAndSend("", "hello3", msg, new CorrelationData(id));
# return rabbitTemplate.waitForConfirms(5000);
# });
# CORRELATED,异步确认,消息附带CorrelationData
# NONE;不开启
publisher-confirm-type: correlated
# 消息失败回退(从交换机到队列失败,例如routtingkey没有对应的队列)
publisher-returns: true
template:
# 开启消息回退,此项为true,才会执行returncallback方法;
mandatory: true
2.设置确认和回退方法
// 在消息发送处实现 RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnsCallback,可以方便针对性的对消息进行确认和发送补偿,
//一个RabbitTemplate只能设置一个ConfirmCallback方法,所以需要设置RabbitTemplate的作用域为原型模式@Scope("prototype")
@Component
public class SendMessage implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnsCallback{
@Autowired
RabbitTemplate rabbitTemplate;
public void send(String msg,String routingKey,String exchange){
//设置ConfirmCallback
rabbitTemplate.setConfirmCallback(this::confirm);
//设置ReturnsCallback
rabbitTemplate.setReturnsCallback(this::returnedMessage);
rabbitTemplate.convertAndSend(exchange,routingKey,msg);
}
//confirm 方法实现
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
System.out.println("执行了returnConfirm方法");
if (b){
System.out.println("===============消息发送成功");
}else {
System.out.println("===============消息发送失败:"+s);
}
}
//returnback 方法实现
@Override
public void returnedMessage(ReturnedMessage returnedMessage) {
System.out.println("执行了returnCallback方法");
System.out.println(returnedMessage);
}
}
5.2 Consumer Ack 消费端手动签收消息
ack指Acknowledge,确认。 表示消费端收到消息后的确认方式。
有三种确认方式:
- 自动确认:acknowledge=“none”
- 手动确认:acknowledge=“manual”
- 根据异常情况确认:acknowledge=“auto”,(这种方式使用麻烦,不常用)
其中自动确认是指,当消息一旦被Consumer接收到,则自动确认收到,并将相应 message 从 RabbitMQ 的 消息缓存中移除。但是在实际业务处理中,很可能消息接收到,业务处理出现异常,那么该消息就会丢失。如 果设置了手动确认方式,则需要在业务处理成功后,调用channel.basicAck(),手动签收,如果出现异常,则 调用channel.basicNack()方法,做一些日志记录或者针对异常的处理方法,或者让其自动重新发送消息(重回队列 注:重回队列是重回队列头)。
spring:
rabbitmq:
host: 172.168.0.100
port: 5672
username: guo
password: guo
virtual-host: /guoVir
listener:
direct:
# 设置确认模式为手动确认
acknowledge-mode: manual
@Component
public class AckTestReceiver {
@RabbitListener(queues = "topic_queue_second")
public void listen (Message message, Channel channel) throws IOException {
try {
//处理业务
System.out.println("收到消息:"+new String(message.getBody()));
//手动签收
channel.basicAck(message.getMessageProperties().getDeliveryTag(),true);
} catch (Exception e) {
e.printStackTrace();
//拒收,第三个参数,是否重新入队
channel.basicNack(message.getMessageProperties().getDeliveryTag(),true,false);
}
}
}
5.3 消费端限流
假设一个场景,首先,我们RabbitMQ
服务器上积压了有上万条未处理的消息,我们随便打开一个消费端,巨量的消息就会瞬间全部推送过来,但是我们单个消费端是无法同时处理这么多消息的。当数据量特别大的时候,我们对生产端限流肯定是不科学的,因为有时候并发量就是特别大,有时候并发量又特别少,我们无法约束生产端,这是用户的行为。所以我们应该对消费端限流,用于保持消费端的稳定,当消息数量激增的时候很有可能造成资源耗尽,以及影响服务的性能,导致系统的卡顿甚至直接崩溃。
RabbitMQ
给我们提供了QOS
(服务质量保证)功能,即在非自动确认消息的前提下(autoAck 要设置为false
),如果一定数目的消息未被ack
前,RabbitMQ
服务器不会推送新的消息给消费端。以下是限流方法:
channel.basicQos(int prefetchSize, int prefetchCount, boolean global)方法来设置限流的配置。
prefetchSize:表示消息的大小(0的话表示不限制大小)
prefetchCount:表示消息的数量,即允许的未确认(Unacked)最大值
global:true表示该通道下的所有消费者都适用这个策略,而false表示只有当前这一个消费者适用这个策略。
在springboot中使用只需要添加一个配置,边可以实现消费端限流;
spring:
rabbitmq:
host: 172.168.0.100
port: 5672
username: guo
password: guo
virtual-host: /guoVir
listener:
simple:
# 设置确认模式为手动确认
acknowledge-mode: manual
#设置消费者端的最大Unacked数,
prefetch: 2
总结:
在springboot下rabbitmq使用消费端限流,只需要配置以下两点:
- 在 yaml中配置 prefetch属性设置消费端一次拉取多少消息 ;
- 消费端的确认模式一定为手动确认。acknowledge="manual;
5.4 TTL
- TTL 全称 Time To Live(存活时间/过期时间)。
- 当消息到达存活时间后,还没有被消费,会被自动清除。
- RabbitMQ可以对消息设置过期时间,也可以对整个队列(Queue)设置过期时间,如果同时设置,则那个时间短生效那个;
-
对队列设置消息的过期时间,在创建队列的时候通过map参数用x-message-ttl来设置队列过期时间;
/*Queue(String name, boolean durable, boolean exclusive, boolean autoDelete, @Nullable Map<String, Object> arguments) * 参数: * 1.name:多列名称 * 2.durable:是否持久化 * 3.exclusive:exclusive: 是否独占。只能有一个消费者监听这队列,当Connection关闭时,是否删除队列 * 4.autoDelete:自动删除,当没有消费者监听队列时候,是否自动删除队列 * 5.arguments可配参数: * 1. x-message-ttl发送到队列的消息在委奔之煎可以存活多长时间_(毫秒) * 2.x-expires队列在被自动删除(毫秒)之前可以使思多长时间。 * 3. x-max-length 队列在开始从头部删除之前可以包含多少就绪消息。 * 4. x-max-length-bytes 队列在开始从头部删除之前可以包含的就绪消息的总体大小。 * 5. x-dead-letter-exchange 死信队列 * 6. x-dead-letter-routing-key 死信队列路由key * 7. x-max-priority 队列支持的最大优先级数;如果未设置,队列将不支持消息优先级。 * 8. x-queue-mode将队列设置为延迟模式,在磁盘上保留尽可能多的消息以减少内存使用;如果未设置,队列将保留内存缓存以尽 * 快传递消息。 * 9. x-queue-master-locator 将队列设置为主位置模式,确定在节点集群上声明时队列主机所在的规则。 * * */ Map<String,Object> param = new HashMap<>(); //设置该队列中消息的过期时间为8秒 param.put("x-message-ttl",8000); Queue queue = new Queue(QUEUE_FIRST,true,false,false,param);
-
对单个消息设置过期时间
/*redisTemplate.convertAndSend(String exchange, String routingKey, Object message, MessagePostProcessor messagePostProcessor) 参数: 1.exchange:交换机名称 2.routingKey 路由key 3.message消息内容 4.MessagePostProcessor //消息的后处理对象,设置一些消息的参数信息,通过这个对象来设置消息的过期时间 */ MessagePostProcessor messagePostProcessor= message->{ //设置消息的过期时间为10秒 message.getMessageProperties().setExpiration("10000"); return message; }; rabbitTemplate.convertAndSend(exchange,routingKey,msg,messagePostProcessor);
**注意:**当队列的消息过期时,如果该条消息并不在队列的头部,并不会马上将过期消息删除。而是等它将要被消费,即到达队头时,再判断它是否到期,到期则删除该消息;
-
总结
设置队列过期时间使用参数:x-message-ttl,单位:ms(毫秒),会对整个队列消息统一过期。
设置消息过期时间使用参数:expiration。单位:ms(毫秒),当该消息在队列头部时(消费时),会单独判断 这一消息是否过期。
如果两者都进行了设置,以时间短的为准。
5.5 死信队列
如果一个消息没有被及时消费,就称这个消息为“死信”,存放死信的队列称为死信队列;
消息成为死信的三种情况:
- 队列消息长度到达限制;
- 消费者拒接消费消息,basicNack/basicReject,并且不把消息重新放入原目标队列,requeue=false;
- 原队列存在消息过期设置,消息到达超时时间未被消费;
死信交换机和死信队列跟普通的交换机和队列没有任何区别,死信交换机跟普通的队列绑定。当消息成为死信后,如果该队列绑定了死信交换机,则消息会被死信交换机重新路由到死信队 列
队列绑定死信交换机:
给队列设置参数: x-dead-letter-exchange 和 x-dead-letter-routing-key
@Bean
//假设死信交换机为 dlx_exchange
public Queue topicFirstQueue() {
Map<String,Object> param = new HashMap<>();
//设置该队列中消息的过期时间为8秒
param.put("x-message-ttl",8000);
//绑定死信队列
param.put("x-dead-letter-exchange","dlx_exchange");
//设置死信队列的路由key
param.put("x-dead-letter-routing-key","dlx.first.ha");
return new Queue(QUEUE_FIRST,true,false,false,param);
}
5.6 延迟队列
延迟队列,即消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费。
需求(应用场景):
-
下单后,30分钟未支付,取消订单,回滚库存。
-
新用户注册成功7天后,发送短信问候。
用rabbitmq实现延迟队列
在RabbitMQ中并未提供延迟队列功能。 但是可以使用:TTL+死信队列 组合实现延迟队列的效果:
生产者将消息发送至队列A,设置队列A的消息过期时间(即为延迟时间)。队列A绑定死信队列(死信交换机)为B。消费者并不直接消费队列A中的消息,而是消费死信队列B中的消息。如此,便可实现延迟队列的功能;
@Configuration
//利用ttl+死信队列实现延时队列
public class DelayQueueConfig {
//正常队列,即生产者发布消息的队列
public static final String NORMAL_QUEUE = "normal_queue";
//死信队列,也是延时队列,正常队列的消息过期后到达此队列,消费端从此队列消费消息;
public static final String DLX_QUEUE = "dlx_queue";
//正常交换机和死信交换机
public static final String NORMAL_EXCHANGE = "normal_exchange";
public static final String DLX_EXCHANGE= "dlx_exchange";
@Bean
public Queue normalQueue (){
//创建队列参数
Map<String,Object> arg = new HashMap<>();
//消息过期时间
arg.put("x-message-ttl",8000);
//死信队列
arg.put("x-dead-letter-exchange",DLX_EXCHANGE);
//死信队列key
arg.put("x-dead-letter-routing-key","delay.queue");
return new Queue(NORMAL_QUEUE,true,false,false,arg);
}
@Bean
public Queue dlxQueue(){
return new Queue(DLX_QUEUE);
}
@Bean
public DirectExchange normalExchange(){
return new DirectExchange(NORMAL_EXCHANGE);
}
@Bean
public DirectExchange dlxExchange(){
return new DirectExchange(DLX_EXCHANGE);
}
//绑定正常交换机和队列
@Bean
public Binding normla(){
return BindingBuilder.bind(normalQueue()).to(normalExchange()).with("delay.queue");
}
//绑定死信交换机跟队列
@Bean
public Binding dlx(){
return BindingBuilder.bind(dlxQueue()).to(dlxExchange()).with("delay.queue");
}
}
总结:
- 延迟队列 指消息进入队列后,可以被延迟一定时间,再进行消费。
-
- RabbitMQ没有提供延迟队列功能,但是可以使用 : TTL + DLX 来实现延迟队列效果。
5.7 日志与监控
RabbitMQ默认日志存放路径: /var/log/rabbitmq/rabbit@xxx.log
日志包含了RabbitMQ的版本号、Erlang的版本号、RabbitMQ服务节点名称、cookie的hash值、 RabbitMQ配置文件地址、内存限制、磁盘限制、默认账户guest的创建以及权限配置等等。
rabbitmqctl命令可用来管理和监控rabbitmq的信息(和web页面监控功能相似)
rabbitmqctl list_queues #查看队列
rabbitmqctl list_exchanges #查看交换机
rabbitmqctl list_users #查看用户
rabbitmqctl list_connections #查看连接
rabbitmqctl list_consumers #查看消费者信息
rabbitmqctl environment #查看环境变量
rabbitmqctl list_queues name messages_unacknowledged #查看未被确认的队列
rabbitmqctl list_queues name memory #查看单个队列的内存使用
rabbitmqctl list_queues name messages_ready #查看准备就绪的队列
5.8 消息追踪
在使用任何消息中间件的过程中,难免会出现某条消息异常丢失的情况。对于RabbitMQ而言,可能 是因为生产者或消费者与RabbitMQ断开了连接,而它们与RabbitMQ又采用了不同的确认机制;也 有可能是因为交换器与队列之间不同的转发策略;甚至是交换器并没有与任何队列进行绑定,生产者 又不感知或者没有采取相应的措施;另外RabbitMQ本身的集群策略也可能导致消息的丢失。这个时 候就需要有一个较好的机制跟踪记录消息的投递过程,以此协助开发和运维人员进行问题的定位。 在RabbitMQ中可以使用Firehose或者rabbitmq_tracing插件功能来实现消息追踪。
-
Firehose
firehose的机制是将生产者投递给rabbitmq的消息,rabbitmq投递给消费者的消息按照指定的格式 发送到默认的exchange上。这个默认的exchange的名称为amq.rabbitmq.trace,它是一个topic类 型的exchange。发送到这个exchange上的消息的routing key为 publish.exchangename 和 deliver.queuename。其中exchangename和queuename为实际exchange和queue的名称,分别 对应生产者投递到exchange的消息,和消费者从queue上获取的消息。我们只需要创建队列来和amq.rabbitmq.trace交换机绑定,便可在队列中记录消息的日志信息;
rabbitmqctl trace_on #开启Firehose命令 rabbitmqctl trace_off #关闭Firehose命令
注意:打开 trace 会影响消息写入功能,生产环境慎用。
-
rabbitmq_tracing
rabbitmq_tracing和Firehose在实现上如出一辙,只不过rabbitmq_tracing的方式比Firehose多了一 层GUI的包装,更容易使用和管理,不需要手动绑定队列
只需要启用插件:rabbitmq-plugins enable rabbitmq_tracing,便可在web页面记录消息日志信息;
5.9 常用消息补偿方案
生产者发送消息钱先将消息保存至DB,发送消息到Q1,如果消费端成功消费,发送确认消息到Q2,然后由回调检查服务将消费成功的消息保存到数据库MDB(该数据库保存的全是消费成功的消息)。为了保证消息的成功率,在第一次发送完之后再利用延迟队列重复发送一次消息到Q3,然后回调检查服务检查MDB中是否已经存在该条消息,如果已存在,证明消息已成功消费。否则,调用生产者重新发送该消息;
为了避免消息第一次发送和延迟队列发送都失败二导致的消息丢失的情况。再开启一个定时检查服务。每隔一段时间来比对MDB和DB(生产者)的数据差。如果存在DB中有而MDB中没有的消息,则是没有成功消费的消息,再调用生产者将这些消息重新发送。
5.10 消息幂等性保障
幂等性指一次和多次请求某一个资源,对于资源本身应该具有同样的结果。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同(请注意这里说的影响是指资源本身,而并不是返回结果)
在MQ中指,消费多条相同的消息,得到与消费该消息一次相同的结果,有如下解决方法:
- 消费数据为了单纯的写入数据库,可以先根据主键查询数据是否已经存在,如果已经存在了就没必要插入了。或者直接插入也没问题,因为可以利用主键的唯一性来保证数据不会重复插入,重复插入只会报错,但不会出现脏数据。
- 消费数据只是为了缓存到redis当中,这种情况就是直接往redis中set value了,天然的幂等性。
- 针对复杂的业务情况,可以在生产消息的时候给每个消息加一个全局唯一ID,消费者消费消息时根据这个ID去redis当中查询之前是否消费过。如果没有消费过,就进行消费并将这个消息的ID写入到redis当中。如果已经消费过了,就无需再次消费了(可直接用setnx实现)。
5.11 RabbitMQ如何保证消息的顺序性
-
出现消费顺序错乱的情况
- 为了提高处理效率,一个queue存在多个consumer
- 一个queue只存在一个consumer,但是为了提高处理效率,consumer中使用了多线程进行处理
-
保证消息顺序性的方法
- 将原来的一个queue拆分成多个queue,每个queue都有一个自己的consumer。该种方案的核心是生产者在投递消息的时候根据业务数据关键值(例如订单ID哈希值对订单队列数取模)来将需要保证先后顺序的同一类数据(同一个订单的数据) 发送到同一个queue当中。
- 一个queue就一个consumer,在consumer中维护多个内存队列,根据业务数据关键值(例如订单ID哈希值对内存队列数取模)将消息加入到不同的内存队列中,然后多个真正负责处理消息的线程去各自对应的内存队列当中获取消息进行消费。