RabbitMQ 是具有代表性的开源消息中间件,当前较多地应用于企业系统内,用于对数据一致性、稳定性和可靠性要求较高的场景中。 RabbitMQ使用Erlang语言来编写的, 并且RabbitMQ是基于AMQP协议的;(AMQP是二进制协议, 提供统一消息服务的应用层标准高级消息队列协议, 是应用层协议的一个开放标准, 为面向消息的中间件设计)
mq消息中间件主要在需要进行异步通信的应用情景中使用,例如:
应用需要确保消息的可靠传递,即使发送消息时接收者由于断电、宕机或 CPU 负载过高等原因不可用,消息也可以在接收者可用时被送达。
需要在访问量与日俱增、囤积在队列中的消息数日益增长的情况下也能正常运转。
两个服务在网络不能互通或者应用的路由信息(例如 IP 和端口)不确定的情况下需要通信。例如,两个微服务在不知道对方地址的情况下需要进行通信,则可以通过约定队列名,一个向队列发送消息,一个从队列中收取消息而实现。
系统组件之间或者应用之间通信较多,需要组件或者应用自身维护彼此的网络连接,而且通信的内容不仅一种。
AMQP协议模型:
AMQP核心概念:
Server : 又称Broker, 接受客户端连接, 实现AMQP实体服务
Connection : 连接, 应用程序与Broker的网络连接
Channel : 网络信道, 几乎所有的操作都在Channel中进行, Channel是进行消息读写的通道。客户端可以建立多个Channel, 每个Channel代表一个会话任务。
Message : 消息, 服务器和应用程序之间传送的数据, 有Properties和Body组成。Properties可以对消息进行修饰, 比如消息的优先级, 延迟等高级特性; Body就是消息体内容。
Virtual Host : 虚拟地址, 用于进行逻辑隔离, 最上层的消息路由。一个Virtual Host里面可以有若干个Exchange和Queue, 同一个Virtual Host里面不能有相同名称的Exchange或Queue
Exchange : 交换机, 用于接收消息, 根据路由键转发消息到绑定的队列
Binding : Exchange和Queue之间的虚拟连接, binding中可以包含routing key
Routing Key : 一个路由规则, 虚拟机可用它来确定如何路由一个特定消息
Queue : 也成Message Queue, 消息队列, 用于保存消息并将它们转发给消费者
1、Exchange:
如果不指定Exchange的话,RabbitMQ默认使用,(AMQP default)注意一下,需要将routing key等于queue name相同
2、交换机类型:
fanout(效率最好,不需要routing key,routing key如何设置都可以)、direct、topic(#一个或多个,*一个)、headers
3、Auto Delete:
当最后一个Binding到Exchange的Queue删除之后,自动删除该Exchange
4、Binding:
Exchange和Queue之间的连接关系,Exchange之间也可以Binding
5、Queue:
实际物理上存储消息的
6、Durability:
是否持久化,Durable:是,即使服务器重启,这个队列也不会消失,Transient:否
7、Exclusive:
这个queue只能由一个exchange监听restricted to this connection,使用场景:顺序消费
8、Message:
由properties(有消息优先级、延迟等特性)和Body(Payload消息内容)组成,还有content_type、content_encoding、priority、correlation_id、reply_to、expiration、message_id等属性
<!-- rabbitmq已经被spring-boot做了整合访问实现。spring cloud也对springboot做了整合逻辑。所以rabbitmq的依赖可以在spring cloud中直接使用。
-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
spring:
rabbitmq:
username: guest
password: guest
addresses: localhost:5672
listener:
type: simple
simple:
concurrency: 5
max-concurrency: 20
acknowledge-mode: manual #设置手动确认
retry:
enabled: true
max-attempts: 3
initial-interval: 1000ms #尝试时间间隔
default-requeue-rejected: false #重试失败后是否回队
prefetch: 2
connection-timeout: 5000ms
cache:
channel:
size: 5
publisher-confirms: true #发布者消息确认
publisher-returns: true #发布者消息回调
template:
retry:
enabled: true
max-attempts: 3
initial-interval: 1000ms #尝试时间间隔
virtual-host: /
此类定义了公用的mq常量和队列,交换机,死信队列等bean初始化和绑定
@Configuration
public class RabbitMqConfig{
/**
* Queue Name
*/
public static final String TEST_QUEUE = "test.rabbit.queue";
/**
* The Topic Exchange Name
*/
public static final String TEST_TOPIC_EXCHANGE = "test-topic-exchange";
/**
* Route Key
*/
public static final String TEST_ROUTE_KEY = "test.rabbit.route";
/**
* Dead Queue Name
*/
public static final String DEAD_QUEUE_NAME = "default.dead.queue";
/**
* The Direct Exchange Name of Dead
*/
public static final String DEAD_EXCHANGE_NAME = "default.dead.exchange";
/**
* Default Dead Route
*/
public static final String DEAD_ROUTE_KEY = "default.dead.route";
/*--------------------------------------------------*/
/**
* 创建一个持久的队列,用于死信队列
* @return
*/
@Bean
public Queue deadLetterQueue (){
return new Queue(DEAD_QUEUE_NAME,true);
}
/**
* 创建死信队列交换机
* @return
*/
@Bean
public DirectExchange deadLetterExchange() {
return new DirectExchange(DEAD_EXCHANGE_NAME,true,false);
}
/**
* 当消息在一个队列中变成死信(dead message)之后, 它能被重新publish到另一个Exchange, 这个Exchange就是DLX
* 设置死信队列的exchange和queue, 然后进行绑定
*/
@Bean
public Binding deadQueueBinding (@Qualifier("deadLetterQueue") Queue deadLetterQueue,
@Qualifier("deadLetterExchange") DirectExchange deadLetterExchange){
return BindingBuilder.bind(deadLetterQueue).to(deadLetterExchange).with(DEAD_ROUTE_KEY);
}
/*--------------------------------------------------*/
/**
* 队列
* @return
*/
@Bean(name = "testQueue")
public Queue flightSearchQueue() {
Map<String, Object> deadLetterMap = new HashMap<>();
//设置死信交换机
deadLetterMap.put("x-dead-letter-exchange", DEAD_EXCHANGE_NAME);
//设置死信routingKey
deadLetterMap.put("x-dead-letter-routing-key", DEAD_ROUTE_KEY);
return new Queue(QUEUE_NAME,false,false,false,deadLetterMap);
}
/**
* 交换机
* @return
*/
@Bean(name = "testExchange")
public TopicExchange exchange() {
return new TopicExchange(TOPIC_EXCHANGE_NAME);
}
@Bean
public Binding bindingExchangeMessage(@Qualifier("testQueue") Queue testQueue,
@Qualifier("testExchange") TopicExchange testExchange) {
return BindingBuilder.bind(testQueue).to(testExchange).with(TEST_ROUTE_KEY );
}
}
在使用 RabbitMQ 的时候,作为消息发送方希望杜绝任何消息丢失或者投递失败场景。RabbitMQ 为我们提供了两个选项用来控制消息的投递可靠性模式。
rabbitmq 整个消息投递的路径为:
producer->rabbitmq broker cluster->exchange->queue->consumer
message 从 producer 到 rabbitmq broker cluster 则会返回一个 confirmCallback 。
message 从 exchange->queue 投递失败则会返回一个 returnCallback 。
我们将利用这两个 来callback 控制消息的最终一致性和记录能力;
在这里我们定义RabbitTemplateConfig类作为统一投递处理,单独处理可直接实现RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback 进行代码实现:
@Slf4j
@Configuration
public class RabbitTemplateConfig {
@Autowired
private RabbitTemplate rabbitTemplate;
@Bean(name = "definedRabbitTemplate")
public RabbitTemplate rabbitTemplateWithConfirmAndCallBack (){
rabbitTemplate.setConfirmCallback((CorrelationData correlationData, boolean ack, String cause)
->{
if(ack){
log.debug("RabbitMq send message success");
}else {
log.error("RabbitMq send message failed, cause:{}", cause);
}
}
);
rabbitTemplate.setReturnCallback((Message message, int replyCode, String replyText, String exchange, String routingKey)
-> log.error("Send message:{} to Queue:{} failed",message,routingKey));
return rabbitTemplate;
}
}
我们在需要发送消息的服务上定义一个消息投递者,如下demo:
@Component
@Slf4j
public class RabbitMqSender{
@Autowired
RabbitTemplate rabbitTemplate;
//业务类处理,此处模拟流程
@Autowired
TestService testService;
/**
* 发送消息demo
* testMessage 模拟业务消息体,作为消息数据载体的类型,必须是Serializable的
* 如果消息数据载体类型未实现Serializable,在收发消息的时候,都会有异常发生。
*/
public void sendMsgRequest(TestMessage testMessage) throws IOException {
try {
rabbitTemplate.convertAndSend(RabbitConfig.TEST_TOPIC_EXCHANGE,
RabbitConfig.TEST_ROUTE_KEY,
JsonUtils.serialize(testMessage), //消息进行序列化处理
new CorrelationData(UUID.randomUUID().toString()));
log.debug("RabbitMq send Message success,testMessage content:{}", testMessage);
} catch (Exception e) {
log.error("RabbitMq send Message ->{} failed,{}", testMessage, e);
}
}
}
作为消费端,需要考虑到消息的可靠性送达,消息回溯,顺序消费以及日志记录,补偿机制等一系列问题,此处不涉及过多场景,消费端处理还需要基于业务模型进行控制,规划;
@Component
@Slf4j
public class RabbitMqReceive {
@Autowired
private TestService testService;
@RabbitListener(queues = {"${test.rabbit.queue}"}, errorHandler = "myRabbitListenerErrorHandler")
public void receiveMsgRequest(String message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws Exception {
try {
TestMessage testMessage = JsonUtils.deserialize(message, TestMessage .class);
log.debug(MessageFormat.format("Consume the queue {0}", RabbitConfig.TEST_QUEUE));
//进行相关业务处理
var response = testService......
//......
//......
if (response.getData() != null) {
//手动消费成功
channel.basicAck(tag, false);
} else {
//手动消费失败,消息不回发
channel.basicNack(tag, false, false);
}
} catch (Exception e) {
//手动消费失败,消息不回发
channel.basicNack(tag, false, false);
}
}
}
@Component
@Slf4j
public class MyRabbitListenerErrorHandler implements RabbitListenerErrorHandler {
@Override
public Object handleError(Message amqpMessage, org.springframework.messaging.Message<?> message, ListenerExecutionFailedException exception) {
log.error("RabbitMQ execute message->{} failed,cause->{}",amqpMessage,exception.getCause());
return new Object();
}
}
前面内容,如果consumer未启动,而producer发送了消息。则消息会丢失。如果consumer先启动,创建queue后,producer发送消息可以正常消费。那么当所有的consumer宕机的时候,queue会auto-delete,消息仍旧会丢失。这种情况,消息不可靠。有丢失的可能。
Rabbitmq的消息可靠性处理,分为两部分。
@Queue注解中的属性 - autoDelete:当所有消费客户端连接断开后,是否自动删除队列 。true:删除 false:不删除
@Exchange注解中的属性 - autoDelete:当交换器所有的绑定队列都不再使用时,是否自动删除交换器(更粗粒度,不建议)。true:删除 false:不删除
如果在消息处理过程中,消费者的服务器在处理消息时发生异常,那么这条正在处理的消息就很可能没有完成消息的消费,如果RabbitMQ在Consumer消费消息后立刻删除消息,则可能造成数据丢失。为了保证数据的可靠性,RabbitMQ引入了消息确认机制。
消息确认机制是消费者Consumer从RabbitMQ中收到消息并处理完成后,反馈给RabbitMQ的,当RabbitMQ收到确认反馈后才会将此消息从队列中删除。
如果某Consumer在处理消息时出现了网络不稳定,服务器异常等现象时,那么就不会有消息确认反馈,RabbitMQ会认为这个消息没有正常消费,会将消息重新放入队列中。
如果在Consumer集群环境下,RabbitMQ未接收到Consumer的确认消息时,会立即将这个消息推送给集群中的其他Consumer,保证不丢失消息。
如果Consumer没有确认反馈,RabbitMQ将永久保存消息。
消息确认机制默认都是开启状态的,同时不推荐关闭消息确认机制。
注意:如果Consumer没有处理消息确认,将导致严重后果。
如:所有的Consumer都没有正常反馈确认信息,并退出监听状态,消息则会永久保存,并处于锁定状态,直到消息被正常消费为止。
消息的发送者Producer如果持续发送消息到RabbitMQ,那么消息将会堆积,持续占用RabbitMQ所在服务器的内存,导致“内存泄漏”问题。
通过编码处理异常的方式,保证消息确认机制正常执行。这种处理方案也可以有效避免消息的重复消费。
异常处理,不是让Consumer编码catch异常后,直接丢弃消息,或反馈ACK确认消息。而是做异常处理的。该抛的异常,还得抛,保证ACK机制的正常执行。或者使用其他的手法,实现消息的再次处理。如:catch代码块中,将未处理成功的消息,重新发送给MQ。如:catch代码中,本地逻辑的重试(使用定时线程池重复执行任务3次。)
通常来说,消息重试3次以上未处理成功,就是Consumer开发出现了严重问题。需要修改Consumer代码,提升版本/打补丁之类的处理方案。
通过全局配置文件,开启消息消费重试机制,配置重试次数。当RabbitMQ未收到Consumer的确认反馈时,会根据配置来决定重试推送消息的次数,当重试次数使用完毕,无论是否收到确认反馈,RabbitMQ都会删除消息,避免内存泄漏的可能。具体配置如下:
#开启重试
spring.rabbitmq.listener.retry.enabled=true
#重试次数,默认为3次
spring.rabbitmq.listener.retry.max-attempts=5