文章目录
消息可靠性
生产者消息确认
RabbitMQ提供了publisher confirm
机制来避免消息发送到MQ过程中丢失。这种机制必须给每个消息指定一个唯一ID
。消息发送到MQ以后,会返回一个结果给发送者,表示消息是否处理成功。
返回结果有两种方式:
- publisher-confirm,发送者确认
- 消息成功投递到交换机,返回ack
- 消息未投递到交换机,返回nack
- publisher-return,发送者回执
- 消息投递到交换机了,但是没有路由到队列。返回ACK,及路由失败原因
注意!
修改配置
修改publisher
服务中的application.yml文件,添加下面的内容:
spring:
rabbitmq:
publisher-confirm-type: correlated
publisher-returns: true
template:
mandatory: true
说明:
publish-confirm-type
:开启publisher-confirm,这里支持两种类型:simple
:同步等待confirm结果,直到超时correlated
:异步回调,定义ConfirmCallback,MQ返回结果时会回调这个ConfirmCallback
publish-returns
:开启publish-return功能,同样是基于callback机制,不过是定义ReturnCallbacktemplate.mandatory
:定义消息路由失败时的策略。true,则调用ReturnCallback;false:则直接丢弃消息
定义Return回调
每个RabbitTemplate只能
配置一个ReturnCallback,因此需要在项目加载时配置
:
修改publisher服务,添加一个:
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Configuration;
@Slf4j
@Configuration
public class CommonConfig implements ApplicationContextAware {
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
// 获取RabbitTemplate
RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
// 设置ReturnCallback
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
// 投递失败,记录日志
log.info("消息发送失败,应答码{},原因{},交换机{},路由键{},消息{}",
replyCode, replyText, exchange, routingKey, message.toString());
// 如果有业务需要,可以重发消息
});
}
}
定义ConfirmCallback
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Component
public class CustomConfirmCallback implements RabbitTemplate.ConfirmCallback {
private static final Logger logger = LoggerFactory.getLogger (CustomConfirmCallback.class);
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* PostConstruct: 用于在依赖关系注入完成之后需要执行的方法上,以执行任何初始化.
*/
@PostConstruct
public void init() {
//指定 ConfirmCallback
rabbitTemplate.setConfirmCallback (this);
}
@Override
public void confirm(CorrelationData correlationData, boolean isSendSuccess, String error) {
logger.info ("(start)生产者消息确认=========================");
if (!isSendSuccess) {
logger.info ("消息可能未到达rabbitmq服务器");
}
logger.info ("(end)生产者消息确认=========================");
}
}
ConfirmCallback可以在发送消息时指定
,因为每个业务处理confirm成功或失败的逻辑不一定相同
在publisher
public void testSendMessage2SimpleQueue() throws InterruptedException {
// 1.消息体
String message = "hello, spring amqp!";
// 2.全局唯一的消息ID,需要封装到CorrelationData中
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
// 3.发送消息
rabbitTemplate.convertAndSend("task.direct", "task", message, correlationData);
// 休眠一会儿,等待ack回执
Thread.sleep(2000);
}
消息持久化
生产者确认
可以确保消息投递到RabbitMQ的队列
中,但是消息发送到RabbitMQ以后,如果突然宕机,也可能导致消息丢失。
要想确保消息在RabbitMQ中安全保存,必须开启消息持久化机制
- 交换机持久化
- 队列持久化
交换机持久化
RabbitMQ中
交换机
默认是非持久化
的,mq重启后就丢失
@Bean
public DirectExchange simpleExchange(){
// 三个参数:交换机名称、是否持久化、当没有queue与其绑定时是否自动删除autoDelete
return new DirectExchange("simple.direct", true, false);
}
队列持久化
RabbitMQ中
队列
默认是非持久化
的,mq重启后就丢失
@Bean
public Queue simpleQueue(){
// 使用QueueBuilder构建队列,durable就是持久化的
return QueueBuilder.durable("simple.queue").build();
}
消费者消息确认
设想这样的场景:
- 1)RabbitMQ
投递
消息给消费者 - 2)消费者获取消息后,
返回ACK
给RabbitMQ - 3)RabbitMQ删除消息
- 4)消费者宕机,消息尚未处理
这样,消息就丢失了。因此消费者返回ACK的时机非常重要
RabbitMQ支持消费者确认机制,即:消费者处理消息后可以向MQ发送ack回执,MQ收到ack回执后才会删除该消息
。而SpringAMQP则允许配置三种确认模式:
•manual:手动ack,需要在业务代码结束后,调用api发送ack。
•auto:自动ack,由spring监测listener代码是否出现异常,没有异常则返回ack;抛出异常则返回nack
•none:关闭ack,MQ假定消费者获取消息后会成功处理,因此消息投递后立即被删除
演示auto模式
修改
consumer
服务的application.yml文件,添加下面内容
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: auto # 开启ack
演示manual模式
public void basicAck(long deliveryTag, boolean multiple):
deliveryTag 表示该消息的index(long类型的数字);
multiple 表示是否批量(true:将一次性ack所有小于deliveryTag的消息);
public void basicNack(long deliveryTag, boolean multiple, boolean requeue):告诉服务器这个消息我拒绝接收,basicNack可以一次性拒绝多个消息。
deliveryTag: 表示该消息的index(long类型的数字);
multiple: 是否批量(true:将一次性拒绝所有小于deliveryTag的消息);
requeue:指定被拒绝的消息是否重新回到队列;
public void basicReject(long deliveryTag, boolean requeue):也是用于拒绝消息,但是只能拒绝一条消息,不能同时拒绝多个消息。
deliveryTag: 表示该消息的index(long类型的数字);
requeue:指定被拒绝的消息是否重新回到队列;
acknowledge-mode: manual 改为手动签收
@RabbitListener(queues = "message_confirm_queue")
public void receiveMessage(String msg, Channel channel, Message message) throws IOException {
try {
// 这里模拟一个空指针异常,
String string = null;
string.length ();
log.info ("【Consumer01成功接收到消息】>>> {}", msg);
// 确认收到消息,只确认当前消费者的一个消息收到
channel.basicAck (message.getMessageProperties ().getDeliveryTag (), false);
} catch (Exception e) {
if (message.getMessageProperties ().getRedelivered ()) {
log.info ("【Consumer01】消息已经回滚过,拒绝接收消息 : {}", msg);
// 拒绝消息,并且不再重新进入队列
//public void basicReject(long deliveryTag, boolean requeue)
channel.basicReject (message.getMessageProperties ().getDeliveryTag (), false);
} else {
log.info ("【Consumer01】消息即将返回队列重新处理 :{}", msg);
//设置消息重新回到队列处理
// requeue表示是否重新回到队列,true重新入队
//public void basicNack(long deliveryTag, boolean multiple, boolean requeue)
channel.basicNack (message.getMessageProperties ().getDeliveryTag (), false, true);
}
e.printStackTrace ();
}
}
消费失败重试机制
当消费者出现异常后
,消息会不断requeue(重入队)到队列,再重新发送给消费者,然后再次异常,再次requeue,无限循环,导致mq的消息处理飙升,带来不必要的压力
本地重试
利用Spring的retry机制,在消费者出现异常时利用本地重试,而不是无限制的requeue到mq队列
修改
consumer
服务的application.yml文件,添加内容
spring:
rabbitmq:
listener:
simple:
retry:
enabled: true # 开启消费者失败重试
initial-interval: 1000 # 初识的失败等待时长为1秒
multiplier: 1 # 失败的等待时长倍数,下次等待时长 = multiplier * last-interval
max-attempts: 3 # 最大重试次数
stateless: true # true无状态;false有状态。如果业务中包含事务,这里改为false
结论:
- 开启本地重试时,消息处理过程中抛出异常,不会requeue到队列,而是在
消费者本地重试
- 重试达到最大次数后,Spring会返回ack,消息会被丢弃
失败策略
在开启重试模式后
,重试次数耗尽,如果消息依然失败
,则需要有MessageRecovery接口来处理,它包含三种不同的实现:
-
RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认就是这种方式
-
ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队
-
RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机
比较优雅的一种处理方案是RepublishMessageRecoverer,失败后将消息投递到一个指定的,专门存放异常消息的队列
,后续由人工集中处理
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.retry.MessageRecoverer;
import org.springframework.amqp.rabbit.retry.RepublishMessageRecoverer;
import org.springframework.context.annotation.Bean;
@Configuration
public class ErrorMessageConfig {
@Bean
public DirectExchange errorMessageExchange(){
return new DirectExchange("error.direct");
}
@Bean
public Queue errorQueue(){
return new Queue("error.queue", true);
}
@Bean
public Binding errorBinding(Queue errorQueue, DirectExchange errorMessageExchange){
return BindingBuilder.bind(errorQueue).to(errorMessageExchange).with("error");
//with就是指定key
}
@Bean
public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){
return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");
}
}
延迟队列
使用
死信队列+TTL
模拟延迟队列
一个队列中的消息如果超时未消费
,则会变为死信
,超时分为两种情况:
- 消息所在的
队列设置了超时时间
消息本身
设置了超时时间
创建队列和交换机
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.QueueBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
@Component
public class DelayMessageConfig {
// 死信队列
public static final String DEAD_LETTERS_QUEUE_NAME = "dl.ttl.queue";
// 死信交换机
private static final String DEAD_LETTERS_EXCHANGE_NAME = "dl.ttl.direct";
// 目标队列
private static final String QUEUE_NAME = "ttl.queue";
// 目标消息交换机
private static final String EXCHANGE_NAME = "ttl.direct";
// ROUTING_KEY
private static final String ROUTING_KEY = "ttl";
/**
* 注册 目标队列和交换机
* 信息往目标队列发送,然后 队列或者消息到期后 将消息发送到死信交换机
* 死信交换机根据key 路由到死信队列
*
* @return
*/
@Bean
public Queue ttlQueue() {
return QueueBuilder.durable (QUEUE_NAME) // 指定队列名称,并持久化
.ttl (10000) // 设置队列的超时时间,10秒
.deadLetterExchange (DEAD_LETTERS_EXCHANGE_NAME) // 指定死信交换机
.build ();
}
@Bean
public DirectExchange ttlExchange() {
return new DirectExchange (EXCHANGE_NAME);
}
@Bean
public Binding ttlBinding(Queue ttlQueue, DirectExchange ttlExchange) {
return BindingBuilder.bind (ttlQueue).to (ttlExchange).with (ROUTING_KEY);
}
/**
* 注册死信队列和交换机
*
* @return
*/
@Bean
public Queue dlQueue() {
return new Queue (DEAD_LETTERS_QUEUE_NAME);
}
@Bean
public DirectExchange dlExchange() {
return new DirectExchange (DEAD_LETTERS_EXCHANGE_NAME);
}
@Bean
public Binding dlBinding(Queue dlQueue, DirectExchange dlExchange) {
return BindingBuilder.bind (dlQueue).to (dlExchange).with (ROUTING_KEY);
}
}
接收消息
import com.rabbitmq.client.Channel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Random;
import static com.example.rabbit.config.DelayMessageConfig.DEAD_LETTERS_QUEUE_NAME;
@Component
public class DelayMessageReceiver {
private static final Logger logger = LoggerFactory.getLogger (DelayMessageReceiver.class);
@RabbitListener(queues = DEAD_LETTERS_QUEUE_NAME)
public void receiveMessage(String receiveMessage, Message message, Channel channel) {
try {
logger.info ("【Consumer】接收到消息:[{}]", receiveMessage);
//这里模拟随机拒绝一些消息到死信队列中
if (new Random ().nextInt (10) < 5) {
logger.info ("【Consumer】拒绝一条信息:[{}],该消息将会被转发到死信交换器中", receiveMessage);
channel.basicNack (message.getMessageProperties ().getDeliveryTag (), false, false);
} else {
channel.basicAck (message.getMessageProperties ().getDeliveryTag (), false);
}
} catch (Exception e) {
logger.info ("【Consumer】接消息后的处理发生异常", e);
try {
channel.basicAck (message.getMessageProperties ().getDeliveryTag (), false);
} catch (IOException e1) {
logger.error ("手动确认消息异常.", e1);
}
}
}
}
发送消息,不指定TTL:
@Test
public void testTTLQueue() {
// 创建消息
String message = "hello, ttl queue";
// 消息ID,需要封装到CorrelationData中
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
// 发送消息
rabbitTemplate.convertAndSend("ttl.direct", "ttl", message, correlationData);
// 记录日志
log.debug("发送消息成功");
}
发送消息的日志:
查看下接收消息的日志:
因为队列的TTL值是10000ms
,也就是10秒
。可以看到消息发送与接收之间的时差刚好是10秒
发送消息,设定TTL
在发送消息时,也可以指定TTL:
@Test
public void testTTLMsg() {
// 创建消息
Message message = MessageBuilder
.withBody("hello, ttl message".getBytes(StandardCharsets.UTF_8))
.setExpiration("5000")
.build();
// 消息ID,需要封装到CorrelationData中
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
// 发送消息
rabbitTemplate.convertAndSend("ttl.direct", "ttl", message, correlationData);
log.debug("发送消息成功");
}
查看发送消息日志:
接收消息日志:
发送与接收的延迟只有5秒
。说明当队列、消息都设置了TTL时
,任意一个到期就会成为死信