一、生产者(Publisher)可靠性
1、生产者重连
在ymal中配置一些参数
连接失败重试 非消息发送失败重试 阻塞式重试
2、生产者确认
RabbitMQ有Publisher Confirm和Publisher Return两种确认机制。开启确机制认后,在MQ成功收到消息后会返回确认消息,返回结果有以下几种情况:
- 消息投递到了MQ,但是路由失败。此时会通过Publisher Return返回路由异常原因,然后返回ACK,告知投递成功。
- 临时消息投递到了MQ,并且入队成功,返回ACK,告知投递成功。
- 持久消息投递到了MQ,并且入队完成持久化,返回ACK,告知投递成功。
- 其他情况返回NACK。
代码实现:
1、ymal添加配置
publisher-confirm-type有三种模式:
- none:关闭
- simple:同步等待
- correlated:异步回调
2、每个RabbitTemplate只能配置一个ReturnCallback,因此需要在项目启动过程中配置:
@Configuration
@Slf4j
public class MqConfirmConfig implements ApplicationContextAware {
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
// 获取RabbitTemplate对象
RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
// 配置回调
rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
@Override
public void returnedMessage(ReturnedMessage returnedMessage) {
log.error("消息发送失败:{}", returnedMessage);
log.debug("消息主体:{}", new String(returnedMessage.getMessage().getBody()));
log.debug("应答码:{}", returnedMessage.getReplyCode());
log.debug("描述:{}", returnedMessage.getReplyText());
log.debug("交换机:{}", returnedMessage.getExchange());
log.debug("路由键:{}", returnedMessage.getRoutingKey());
}
});
}
}
3、发送消息,指定消息ID、消息ConfirmCallback
@Test
void testConfirmCallback() throws InterruptedException {
// 创建cd
CorrelationData cd = new CorrelationData(UUID.randomUUID().toString());
// 添加ConfirmcallBack
cd.getFuture().addCallback(new ListenableFutureCallback<CorrelationData.Confirm>() {
@Override
public void onFailure(Throwable ex) {
log.error("消息回调失败:{}",ex.getMessage());
}
@Override
public void onSuccess(CorrelationData.Confirm result) {
log.info("消息回调成功:{}", result);
if (result.isAck()) {
log.info("消息发送成功,收到Ack");
}else{
log.error("消息发送失败:{},收到nack",result.getReason());
}
}
});
rabbitTemplate.convertAndSend("hmall.direct","red","hello",cd);
Thread.sleep(2000);
}
尽量不要用 无需开启Publisher-Return 有限次重试
二、MQ可靠性
在默认情况下,RabbitMQ会将接收到的信息保存在内存中以降低消息收发的延迟。这样会导致两个问题:
- 一旦MQ宕机,内存中的消息会丢失
- 内存空间有限,当消费者故障或处理过慢时,会导致消息积压,引发MQ阻塞
1、数据持久化
- 交换机持久化(spring中默认为持久化)
- 队列持久化(spring中默认持久化)
- 消息持久化(spring中默认持久化)
消息持久化在发送时将delivery_mode改为2
2、LazyQueue(惰性队列)
从RabbitMQ的3.6.0版本开始,就增加了LazyQueue的概念,也就是惰性队列。
惰性队列的特征如下:
- 接收到消息后直接存入磁盘而非内存(内存中只保留最近的消息,默认2048条)
- 消费者要消费消息时才会从磁盘中读取并加载到内存
- 支持数百万条的消息存储
在3.12版本后,所有队列都是Lazy Queue模式,无法更改。
在控制台创建队列的时候可以选择Lazy mode
java基于注解可以增加一个属性
三、消费者可靠性
1、消费者确认机制
为了确认消费者是否成功处理消息,RabbitMQ提供了消费者确认机制(ConsumerAcknowledgement)
当消费者处理消息结束后,应该向RabbitMO发送一个回执,告知RabbitM0自己消息处理状态。回执有三种可选值:
- ack:成功处理消息,RabbitMO从队列中删除该消息
- nack:消息处理失败,RabbitMQ需要再次投递消息
- reject:消息处理失败并拒绝该消息,RabbitMQ从队列中删除该消息
SpringAMQP已经实现了消息确认功能。并允许我们通过配置文件选择ACK处理方式,有三种方式:
- none:不处理。即消息投递给消费者后立刻ack,消息会立刻从MQ删除。非常不安全,不建议使用
- manual:手动模式。需要自己在业务代码中调用api,发送ack或reject,存在业务入侵,但更灵活
- auto:自动模式。SpringAMQP利用AOP对我们的消息处理逻辑做了环绕增强,当业务正常执行时则自动返回ack.
当业务出现异常时,根据异常判断返回不同结果:
如果是业务异常,会自动返回nack
如果是消息处理或校验异常,自动返回reject
在ymal文件中配置:acknowledge-mode
2、消费失败处理
可以在ymal中配置 可以先本地重试
重试失败后还需要MessageRecoverer接口处理,否则会删除消息
- RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认就是这种方式
- ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队
- RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机
将失败处理策略改为RepublishMessageRecoverer:
- 首先,定义接收失败消息的交换机、队列及其绑定关系,此处略:
- 然后,定义RepublishMessageRecoverer:
@Configuration
@ConditionalOnProperty(prefix = "spring.rabbitmq.listener.simple.retry",name = "enabled",havingValue = "true")
@Slf4j
public class ErrorConfiguration {
@Bean
public DirectExchange errorExchange(){
return new DirectExchange("error.direct");
}
@Bean
public Queue errorQueue(){
return new Queue("error.queue");
}
@Bean
public Binding errorBinding(){
return BindingBuilder.bind(errorQueue()).to(errorExchange()).with("error");
}
@Bean
public MessageRecoverer messageRecoverer(RabbitTemplate rabbitTemplate){
log.info("启用消息重发机制");
return new RepublishMessageRecoverer(rabbitTemplate,"error.direct","error");
}
}
3、业务幂等性
幂等是一个数学概念,用函数表达式来描述是这样的:f(x)=f(f(x))。在程序开发中,则是指同一个业务,执行一次或多次对业务状态的影响是一致的。
1、唯一ID 将处理完的消息id存入数据库
在创建消息转换器时可以赋id
@Bean
public MessageConverter jacksonMessageConverter() {
Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter();
jackson2JsonMessageConverter.setCreateMessageIds(true);
return jackson2JsonMessageConverter;
}
缺点:需要新增操作 影响性能
2、基于业务本身判断
例如新增状态字段,来判断是否执行过操作
四、延迟消息
延迟消息:生产者发送消息时指定一个时间,消费者不会立刻收到消息,而是在指定时间之后才收到消息
延迟任务:设置在一定时间之后才执行的任务
1、死信交换机
当一个队列中的消息满足下列情况之一时,就会成为死信(dead letter)
- 消费者使用basic.reject或 basic.nack声明消费失败,并且消息的requeue参数设置为false
- 消息是一个过期消息(达到了队列或消息本身设置的过期时间),超时无人消费
- 要投递的队列消息堆积满了,最早的消息可能成为死信
但是死信交换机并不是设计来进行延迟消息的
2、延迟消息插件
RabbitMQ的官方也推出了一个插件,原生支持延迟消息功能。该插件的原理是设计了一种支持延迟消息功能的交换机当消息投递到交换机后可以暂存一定时间,到期后再投递到队列。
首先安装插件
交换机创建时新增delay属性变为true
发消息setDelay()
@Test
void testSendDelay() throws InterruptedException {
rabbitTemplate.convertAndSend("delay.direct", "hi","hello", new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
message.getMessageProperties().setDelay(10000);
return message;
}
});
log.info("发送延迟消息成功");
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "delay.queue",durable = "true"),
exchange = @Exchange(name = "delay.direct",type = ExchangeTypes.DIRECT,delayed = "true"),
key = "hi"
))
public void listenDelayQueue(String msg) throws InterruptedException {
//System.out.println("消费者1收到delay.queue消息:"+msg);
log.info("消费者收到delay.queue消息:{}",msg);
}
发消息时设置x-delay来设置过期时间
适用于延迟时间较短