RabbitMQ保证消息的可靠性和幂等性

73 阅读5分钟

RabbitMQ的消息可靠性

上一篇文章我们讲了RabbitMQ的简单使用,但其实在实际的开发中,RabbitMQ会出现一些问题,比如我们向RabbitMQ存放消息时,由于网络问题连接断开,那么消息是不是就丢失了,还有,当临时消息放入MQ队列中,由于MQ宕机或者重启,临时消息就会丢失。我们需要保证MQ的可靠性,让消息至少被消费一次。


生产者可靠性

我们首先保证生产者的可靠性,也就是保证生产者能够将消息成功的放入到MQ中。

失败重试机制

当生产者将消息放入MQ时,如果由于网络问题,与MQ的连接断开,就会导致消息的丢失,那么我们可以多次尝试建立连接,并且控制每次建立连接失败的超时时长。 在springboot配置文件中配置:

  rabbitmq:
    addresses:  ***
    username: ***
    password:  ***
    virtual-host: ***
    port: 5672
#        连接超时时间
    connection-timeout: 1s
    template:
      retry:
#        开启连接的失败重连机制
        enabled: true
#        连接失败后的初始等待时间
        initial-interval: 1000ms
#         之后连接等待时间的倍数
        multiplier: 1
#         最大连接重试的次数        
        max-attempts: 3

生产者确认机制

在成功建立连接之后,还存在一些原因,导致消息存放MQ失败,例如将MQ收到消息后发生故障导致存放MQ失败、MQ收到持久化消息时磁盘满了无法持久化等,我们开启MQ的确认机制,当MQ成功收到消息时,返回ACK,没有成功收到消息时返回NACK,此时我们可以进行重新发送消息。 添加配置信息:

  spring:
   rabbitmq:
    publisher-confirm-type: correlated

type有三种,分别为none、simple、correlate none:不开启确认机制,默认为none simple:同步阻塞等待返回信息 correlate:异步回调方式收到返回信息

通过配置设置回调信息:

@Configuration
@Slf4j
public class MQConfig implements ApplicationContextAware {
    /**
     * 开启生产者确认模式:
     * 当交换机收到消息但是路由失败,返回ACK
     * 当临时消息入队成功,返回ACK
     * 当永久消息入队成功,返回ACK
     * */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
        rabbitTemplate.setReturnCallback(((message, replyCode, replyText, exchange, routingKey) -> {
            log.debug("消息发送失败,应答码{},原因{},交换机{},消息{},路由键{}",replyCode,replyText,exchange,message,routingKey);
        }));
    }
}

MQ可靠性

默认情况下,MQ会将收到的消息放入内存中,来减少消息收发带来的延迟,如果MQ突然宕机,那么内存中的消息就会丢失,因此我们要让交换机、队列和消息持久化,默认情况下它们都是持久化的。

交换机: 在这里插入图片描述 队列: 在这里插入图片描述 消息: 在这里插入图片描述

消费者可靠性

我们要保证消息的可靠性,也就是消息至少被消费者消费一次,如果消费者在消费消息时由于业务抛出了异常,那么这个消息是不是就没有被成功消息,因此我们需要开启消费者的确认机制来保证消息的可靠性。

  spring:
   rabbitmq:
   #开启确认机制
       acknowledge-mode: auto

acknowledge-mode包括三个值,分别是none、manual、Auto none:不进行处理。消息发送给消费者之后,会立即将消息删除 manual:手动处理。在代码中手动发送ack和nack Auto:自动处理。利用Spring Aop对业务进行环绕增强,如果业务正常进行返回ack,如果是业务异常,会返回nack,消息就会循环入队处理直到业务正常执行,如果是检查异常或者消息处理异常,就会返回reject,将消息从队列中删除。 为了防止返回nack时消息无法正常处理,一直循环处理带来不必要的压力,我们可以设置消息的最大重试次数

 spring:
   rabbitmq:
   #开启确认机制
       acknowledge-mode: auto
#        为了防止返回NACK一直重试浪费资源,设置重试次数
        retry:
          enabled: true
          max-attempts: 5

如果消息重试耗尽时,我们可以使用别的策略,例如我们可以将消息发送到一个专门记录错误的队列,后续由人工处理。 设置配置类:

/**
 * 当设置失败重试属性时才将其放入队列中
 * */
@Configuration
@ConditionalOnProperty(prefix = "spring.rabbitmq.listener.simple.retry",name = "enabled",havingValue = "true")
public class ErrorMQConfiguration {
    @Bean
    public DirectExchange directExchange() {
        return new DirectExchange("error.direct");
    }

    @Bean
    public Queue queue(){
        return new Queue("error.queue");
    }

    @Bean
    public Binding binding(){
        return BindingBuilder.bind(queue()).to(directExchange()).with("error");
    }
    /**
     * 重试耗尽之后放入error队列
     * */
    @Bean
    public MessageRecoverer messageRecoverer(RabbitTemplate rabbitTemplate){
        return new RepublishMessageRecoverer(rabbitTemplate,"error.direct","error");
    }
}

消息的幂等性

我们在上面保证了消息的可靠性,也即消息至少被消费一次,在某些情况下消息存在重复消费,例如,在我们开启消费者确认机制后,当消息被消费者处理后会返回ack,队列收到后会将队列中对应的消息删除,如果此时网络产生波动,导致队列没有收到ack,队列会以为没有消费过该消息,再次将消息分发给其他消费者,造成了消息的重复消费。

给消息添加唯一id

我们可以给消息添加一个唯一id,当消息被消费之后,就保存到数据库中,每次消息要被消费时,从数据库查询消息是否被消费,可以保证消息的幂等性,但是同时也会带来一些性能的消耗,我们可以在设置消息转换器时开启创建消息的唯一id

    @Bean
    public MessageConverter messageConverter(){
        Jackson2JsonMessageConverter jjmc = new Jackson2JsonMessageConverter();
        jjmc.setCreateMessageIds(true);
        return jjmc;
    }

在业务中进行幂等判断

我们还可以在业务中进行一些幂等判断,例如一个要保证一人一单的秒杀活动,我们执行业务时,可以判断这个人有没有购买过这个商品,如果购买过直接返回,来保证业务的幂等性

在这里插入图片描述