目录
消息可靠性问题
消息从生产者到exchange,再到queue,再到消费者,有哪些导致消息丢失的可能性?
- 生产者发送的消息未送达exchange 解决办法:生产者消息确认
- 消息到达exchange后未能成功路由到queue 解决办法:生产者消息确认
- RabbitMQ宕机,queue将消息丢失 解决办法:消息持久化
- 消息者接收到消息后还未来得及消费就宕机 解决办法:消息失败重试机制
生产者消息确认
RabbitMQ提供了publisher confirm机制来避免消息发送到MQ过程中丢失。消息发送到MQ以后,会返回一个结果给发送者,表示消息是否处理成功。结果有两种请求:
- publisher-confirm,发送者确认
- 消息成功投递到交换机,返回ack
- 消息未投递到交换机,返回nack
- publisher-return,发送者回执
- 消息投递到交换机了,但是没有路由到队列。返回ACK,及路由失败原因。
我们先把基础框架搭建好:
spring:
rabbitmq:
username: songdiao
password: sd460429
host:
port: 5672
virtual-host: /
@SpringBootTest
public class Publisher {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void test(){
rabbitTemplate.convertAndSend("testExchange","red","测试消息可靠性。。。");
}
}
@Component
public class Consumer {
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue,
exchange = @Exchange(value = "testExchange",type = "direct"),
key = {"red","green","blue"}
)
})
public void test1(String msg){
System.out.println("消费者消费消息:"+msg);
}
}
接下来发送者确认如何实现?
- 编写一个自定义的类,实现ConfirmCallback接口,并且实现其confirm方法,然后交给spring管理
@Component
public class MyConfirmCallback implements RabbitTemplate.ConfirmCallback {//注意此接口是RabbitTemplate包下的
//参数1:数据
//参数2:返回true表示发送成功,false表示发送失败
//参数3:如果发送成功,返回null。如果发送失败,返回失败的原因
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack){
System.out.println("消息成功送达交换机"+cause);
}else {
System.out.println("消息未能送达交换机,失败原因为:"+cause);
}
}
}
- 在消息的发送方(生产者)里将我们自定义的类注入,并设置发送者确认需要执行的是我们自定义的这个类
- 配置文件中开启publisher-confirm功能
然后发送者回执如何实现?
注意:每个RabbitTemplate只能配置一个ReturnCallback
- 还是编写一个自定义的类,然后实现ReturnsCallback接口,且实现ReturnedMessage方法,再交给spring管理
@Component
public class MyReturnsCallback implements RabbitTemplate.ReturnsCallback {
@Override
public void returnedMessage(ReturnedMessage returned) {
//message 消息本身内容
System.out.println("发送的消息:"+new String(returned.getMessage().getBody()));
//replyCode 响应的状态码
System.out.println("状态码:"+returned.getReplyCode());
//replyText 响应的内容
System.out.println("响应的内容:"+returned.getReplyText());
//exchange 交换机
System.out.println("交换机:"+returned.getExchange());
//routingKey
System.out.println("routingKey:"+returned.getRoutingKey());
}
}
- 在消息的发送方(生产者)里将我们自定义的类注入,并设置发送者回执需要执行的是我们自定义的这个类
- 配置文件中开启发送者回执
测试:
- 当消息发送成功时,控制台打印
- 我们把交换机名称修改为一个不存在的交换机,来模拟生产者消息未送达交换机,测试发送者确认功能
- 我们把routingKey修改为一个不能成功匹配的,来模拟生产者消息送达交换机,但是交换机未能把消息路由到队列,测试发送者回执功能
消息持久化
对于原生的开发方式(不使用AMQP):
要设置三个地方,即交换机持久化,队列持久化,消息持久化,我们手动实现方式代码的注释上都有,在此不做赘述。
AMQP方式:
只需要设置交换机持久化和队列持久化即可,因为消息默认就是持久化的。
如下图两个durable属性值的地方就是分别设置队列持久化和交换机持久化的
消费者消息确认
RabbitMQ支持消费者确认机制,即:消费者处理消息后可以向MQ发送ack回执,MQ收到ack回执后才会删除该消息。而SpringAMQP则允许配置三种确认模式:
manual:手动ack。在业务代码成功运行结束后,调用api发送ack(channel.basicAck()),当然,如果出现异常就调用api发送nack(channel.basicNack()),让其按照业务功能进行处理,比如:重新入队,拒绝签收直接丢弃,或者拒绝签收丢弃以后进入死信交换机。
auto:自动ack。由spring监测listener代码是否出现异常,没有异常则返回ack;抛出异常则返回nack,然后重入队列。默认就是auto模式
none:关闭ack。MQ假定消费者获取消息后会成功处理,因此消息投递后立即被删除。
先搭建基础框架:
spring:
rabbitmq:
username: songdiao
password: sd460429
host:
port: 5672
virtual-host: /
@SpringBootTest
public class Publisher {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void test(){
rabbitTemplate.convertAndSend("test1Exchange","","测试消息。。。");
}
}
@Component("test2")
public class Consumer {
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue,
exchange = @Exchange(value = "test1Exchange",type = "fanout")
)
})
public void test(String msg){
System.out.println("消费者消费消息:"+msg);
}
}
接下来消费者手动确认如何实现?
- 我们把消费者的方法修改一下,改为接收三个参数,Message,Channel,String,然后编写手动ack和nack
@Component("test2")
public class Consumer {
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue,
exchange = @Exchange(value = "test1Exchange",type = "fanout")
)
})
//参数1:消息的封装对象,包括消息的顺序号,消息本身body,消费者名称...
//参数2:连接的通道对象
//参数3:消息本身body
public void test(Message message, Channel channel, String msg){
//接收到消息
System.out.println("消费者消费消息:"+msg);
try{
//处理业务
System.out.println("开始处理业务");
Thread.sleep(2000);
System.out.println("业务处理完毕");
//业务处理成功,手动返回ack(相当于签收消息)
//参数1为消息的序号,参数2为是否批量签收,true表示批量,false表示不批量
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}catch (Exception e){
try {
//业务处理异常,手动返回nack(相当于拒绝签收消息)
//参数1为指定消息的序号,参数2为是否批量进行签收。参数3表示拒绝签收以后,是否重回队列,true表示重回队列,而false表示不重回队列,直接丢弃消息
channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,false);
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
}
}
- 在配置文件中修改为手动ack
消息失败重试机制
当消费者出现异常后,(自动ack或者手动ack且重新入队的情况下)消息会不断requeue(重新入队)到队列,再重新发送给消费者,然后再次异常,再次requeue,无限循环,导致mq的消息处理飙升,带来不必要的压力:
先搭建基础框架:
spring:
rabbitmq:
username: songdiao
password: sd460429
host:
port: 5672
virtual-host: /
@SpringBootTest
public class Publisher {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void test()throws Exception{
rabbitTemplate.convertAndSend("test2Exchange","","测试消息。。。");
}
}
@Component("test3")
public class Consumer {
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue("testRetry"),
exchange = @Exchange(value = "test2Exchange",type = "fanout")
)
})
public void test(Message message, Channel channel, String msg){
System.out.println("接收到消息:"+msg);
}
}
解决办法:
我们可以利用Spring的retry机制,在消费者出现异常时利用本地重试,而不是无限制的requeue到mq队列。
在配置文件中配置retry的相关属性
那么又存在一个问题,还在上面配置的基础上如果达到重试次数是失败怎么办?
此时就需要有MessageRecoverer接口来处理,它包含三种不同的实现:
RejectAndDontRequeueRecoverer:重试耗尽后,直接丢弃消息。默认就是这种方式
ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队
RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机。(最常用)
配置RepublishMessageRecoverer重试机制:
实现当达到一定的重试机制以后,该消息就会转移到指定的队列当中
//声明一个交换机和队列,双方绑定,然后专门用来存储达到重试次数以后还是失败的消息
//消息重试一定次数后,用特定的routingKey转发到指定的交换机中,方便后续排查和告警
@Configuration
public class PublisherMessageRecovererConfig {
@Autowired
private RabbitTemplate rabbitTemplate;
//声明一个交换机
@Bean
public DirectExchange errorExchange(){
//参数1为交换机名称。参数2为是否持久化。参数3为当没有queue与其绑定时是否自动删除
return new DirectExchange("errorExchange",true,false);
}
//声明一个队列
@Bean
public Queue errorQueue(){
//参数1为队列名称。参数2为是否持久化。参数3为是否独占。参数4为是否自动删除
return new Queue("errorQueue",true,false,false);
}
//将交换机与队列绑定
@Bean
public Binding errorExchangeAndErrorQueue(){
return BindingBuilder.bind(errorQueue()).to(errorExchange()).with("test");
}
@Bean
public MessageRecoverer messageRecoverer(){
//参数1为RabbitTemplate对象,参数2为交换机名称,参数3为routingKey
return new RepublishMessageRecoverer(rabbitTemplate,"errorExchange","test");
}
}
延迟消息问题
死信交换机
当一个队列中的消息满足下列情况之一时,可以成为死信(deadletter):
- 消费者使用basic.reject或 basic.nack声明消费失败,并且消息的requeue参数设置为false
- 消息是一个过期消息,超时无人消费
- 要投递的队列消息堆积满了,最早的消息可能成为死信
如果该队列配置了dead-letter-exchange属性,指定了一个交换机,那么队列中的死信就会投递到这个交换机中,而这个交换机称为死信交换机(Dead LetterExchange,简称DLX)。
TTL
TTL,也就是Time-To-Live。如果一个队列中的消息TTL结束仍未消费,则会变为死信,ttl超时分为两种情况:
- 消息所在的队列设置了存活时间
- 消息本身设置了存活时间(注意:针对某一个特定的消息设置过期时间时,一定是消息在队列中在队头的时候进行计算,如果某一个消息A 设置过期时间5秒,此时消息B在队头,消息B没有设置过期时间,B此时过了已经5秒钟了还没被消费。注意,此时A消息并不会被删除,因为它并没有在队头。)
延迟队列
利用死信交换机和设置ttl过期时间,来达到延迟消息发送的效果
例如我们声明一个设置过期时间的队列,或者给某条具体的消息设置一个过期时间。然后再声明一个死信交换机。当我们的队列或者消息过期以后以后,消息就会成为死信,然后进入我们的死信交换机以后再被消费,这样就达到了延迟的效果。
spring:
rabbitmq:
username: songdiao
password: sd460429
host:
port: 5672
virtual-host: /
//声明一个队列和一个交换机,并绑定。用来测试ttl
@Configuration
public class ttlConfig {
@Bean
public DirectExchange ttlExchange(){
return new DirectExchange("ttlExchange",false,false);
}
@Bean
public Queue ttlQueue(){
return QueueBuilder.durable("ttlQueue")//设置队列名称并持久化
.ttl(10000)//设置队列超时时间,单位为ms
.deadLetterExchange("deadExchange")//指定死信交换机
.deadLetterRoutingKey("deadTest")//指定死信routingKey
.build();
}
@Bean
public Binding bind(){
return BindingBuilder.bind(ttlQueue()).to(ttlExchange()).with("ttl");
}
}
//编写一个生产者,发送消息到已设置过期时间的队列中
@SpringBootTest
public class Publisher {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void test(){
Message message =MessageBuilder
.withBody("测试消息。。。".getBytes())//消息具体内容
.setExpiration("15000")//设置消息过期时间
.build();
// 消息ID,需要封装到CorrelationData中
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
rabbitTemplate.convertAndSend("ttlExchange","ttl",message,correlationData);
}
}
//编写消费者,声明一个死信交换机和队列,测试队列或者消息过期时,死信进入到此队列
@Component("test4")
public class Consumer {
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue("deadQueue"),
exchange = @Exchange(value = "deadExchange",type = "direct"),
key = {"deadTest"}
)
})
public void test(String msg){
System.out.println("我是死信交换机,我已接收到死信");
}
}
消息堆积问题
消息堆积问题
当生产者发送消息的速度超过了消费者处理消息的速度,就会导致队列中的消息堆积,直到队列存储消息达到上限。最早接收到的消息,可能就会成为死信,会被丢弃,这就是消息堆积问题。
解决消息堆积有三种种思路:
- 增加更多消费者,提高消费速度
- 在消费者内开启线程池加快消息处理速度
- 扩大队列容积,提高堆积上限。(惰性队列)
惰性队列
从RabbitMQ的3.6.0版本开始,就增加了Lazy Queues的概念,也就是惰性队列。
惰性队列的特征如下:
- 接收到消息后直接存入磁盘而非内存
- 消费者要消费消息时才会从磁盘中读取并加载到内存
- 支持数百万条的消息存储。
- 默认情况下生产者消息会尽可能存储到内存中就算设置持久化消息的也会再内存中备份一份,当rabbitMQ 需要释放内存时会将内存中的队列持久化到磁盘中,而惰性队列无论持久化和非持久化都会存储到磁盘,而且就算非持久化设置惰性队列重启后消息也会丢失 (所以持久化消息和惰性队列是很好的搭档)。
- 惰性队列是基于磁盘存储,所以优缺点显而易见。虽然存储消息空间大大提升,但处理消息的性能方面也会略有下降。
- 声明一个队列为惰性队列主要有两种方式,如下:
高可用问题
采用集群,RabbitMQ的集群分为三种:普通集群,镜像集群,以及仲裁队列。
先不做具体