RabbitMQ如何保证消息的可靠性

如何确保RabbitMQ消息的可靠性

  • 对于生产者,开启生产者确认机制,确保生产者的消息能到达队队列

  • 对于mq,开启持久化功能,确保消息未消费前在队列中不会丢失

  • 对于消费者,开启消费者确认机制为auto,有spring确认消息处理成功后完成ack

  • 开启消费者失败重试机制,并设置MessageRevocerer,多次重试失败后会将消息投递到异常交换机,进行人工处理等

MQ一些常见问题

  1. 消息可靠性问题:如何确保发送的消息至少被消费一次

  2. 消息延迟问题:如何实现消息的延迟投递

  3. 消息堆积问题:如何解决数百万消息堆积,无法及时消费的问题

  4. 高可用性问题:避免单点MQ故障而导致的不可用

1、消息可靠性

消息丢失的可能性

  • 发送时丢失:

    • 生产者发送消息未送达exchange

    • 消息到达exchange后未到达queue

  • MQ宕机,queue将消息丢失

  • consumer接受消息后未消费就宕机

生产者消息确认

RabbitMQ提供了publisher confirm机制来避免消息发送到MQ过程中丢失。消息发送到MQ以后,会返回一个结果给发送者,表示消息是否处理成功。结果有两种请求

  • publisher-confirm,发送者确认

    • 消息返回成功投递到交换机,返回ack

    • 消息未成功投递到交换机,返回nack

  • publisher-return,发送者回执

    • 消息投递到交换机了,但是没有路由到队列。返回ack,及路由失败原因

确认机制发送消息时,需要给每个消息设置一个全局唯一ID,以区分不同消息,避免ACK冲突

生产者确认消息案例

1、在publisher服务的application.yml中添加配置
spring: 
  rabbitmq:
    publisher-confirm-type: correlated #异步回调确认类型
    publisher-returns: true #开启publisher-returns的功能
    template:
    mandatory: true #定义路由失败的策略
  • publish-confirm-type:开启publisher-confirm,这里支持两种类型:

    • simple:同步等待confirm结果,直到超时,可能会导致进程阻塞,不建议使用

    • correlated:异步回调,定义ConfirmCllBack。MQ返回结果时会回调这个ConfirmCallBack,有点像ajax,推荐使用

  • publish-returns:开启publish-return功能,同样是基于callback机制,不过是定义ReturnCallback

  • template.mandatory:定义消息路由失败的策略。true,则调用ReturnCallback;false:则直接丢弃消息

2、编写回调函数ReturnCallback

每个RabbitTemplate只能配置一个ReturnCallback,因此需要在项目启动过程中配置

  • ReturnCallback代表你消息到了交换机路由消息失败了,回调此函数

  • 实现ApplicationContextAware接口 —— Spring中bean工厂的通知,当Springbean工厂准备好了以后就来通知你,并且重写setApplicationContext方法,传入一个ApplicationContext工厂对象,通过这个工厂获取bean,获取rabbitTemplate,设置ReturnCallback

  • 五个参数:有了这些参数可以记录日志,有了交换机和rotingKey也可以重新发送消息,实现消息重试

    • message你发的是什么消息

    • replyCode失败状态码

    • replyText失败描述原因

    • exchange交换机是什么

    • routingKey路由key是什么

/**
     * km 重写setAppicationContext方法,传入一个ApplicationContext工厂对象,通过这个工厂获取bean,获取rabbitTemplate,设置ReturnCallback
     *
     * @param applicationContext 工厂对象
     * @throws BeansException 抛出bean异常
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        // kp 获取RabbitTemplate对象
        RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
        // kp 配置ReturnCallback
        // ks 五个参数:消息路由失败了,message你发的是什么消息,replyCode失败状态码。replyText失败描述原因,exchange交换机是什么,routingKey路由key是什么,有了这些参数可以记录日志,也可以重新发送消息,实现消息重试
        rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
            // 判断是否是延迟消息
            Integer receivedDelay = message.getMessageProperties().getReceivedDelay();
            if (receivedDelay != null && receivedDelay > 0) {
                // 是一个延迟消息,忽略这个错误提示
                return;
            }
            // 记录日志
            log.error("消息发送到队列失败,响应码:{}, 失败原因:{}, 交换机: {}, 路由key:{}, 消息: {}",
                     replyCode, replyText, exchange, routingKey, message.toString());
            // 如果有需要的话,重发消息
        });
    }
3、发送消息ConfirmCallback

每个RabbitMQ,可以有多个ConfirmCallback,每次发消息都可以发不同的ConfirmCallback,弄不同的业务处理方案,在每次发消息那一刻配置ConfirmCallBack

  • ConfirmCallback是指消息根本没到交换机

  • 准备一条消息体

  • 生成消息的唯一ID,获取Future并且使用lambda表达式添加callback

    • result是正常的callback回调,受到的回执又分为两种

      • ack成功的回执,消息成功投递到交换机

      • nack消息失败的回执,消息没成功投递到交换机

    • ex是异常的callback回调,就是执行过程中直接没收到回执,抛异常了才会进入失败的水貂

  • 发送消息convertAndSend("交换机的名称", "路由key的名称", "消息体",correlatiohnData)

public void testSendMessage2SimpleQueue() throws InterruptedException {
        // 1.准备消息
        String message = "hello, spring amqp!";
        // ks 2.准备CorrelationData
        // kp 2.1.准备消息的唯一ID
        CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
        // kp 2.2.准备ConfirmCallback
        correlationData.getFuture().addCallback(
            result -> { // kp result正常回调
            // 判断结果
            if (result.isAck()) {
                // kp 发送ACK回执,代表消息成功投递到交换机
                log.debug("消息成功投递到交换机!消息ID: {}", correlationData.getId());
            } else {
                // kp 发送NACK回执,代表消息投递到交换机失败,取得消息id
                log.error("消息投递到交换机失败!消息ID:{}", correlationData.getId());
                // 重发消息
            }
        }, ex -> { //kp ex异常回调
            // 记录日志
            log.error("消息发送失败!", ex);
            // todo 重发消息
        });
        // 3.发送消息
        rabbitTemplate.convertAndSend("amq.topic", "a.simple.test", message, correlationData);
    }

总结SpringAMQP中处理消息确认的几种情况

  • publisher-confirm:

    • 消息成功发送到exchange,返回ack

    • 消息发送失败,没有到达交换机,返回nack

    • 消息发送过程中出现异常,没有受到回执 ex

  • 消息成功发送到exchange,但是没有路由到queue

    • 调用ReturnCallBack

2、消息持久化

宕机消息能否保存到磁盘上

durable参数,代表可持久化,在配置中心可视化配置交换机、队列的时候设置duriability设置为

使用代码

  • MQ默认是内存消息,开启持久化功能可以确保缓存在MQ中的消息不丢失

  • 消费者在启动的时候,可以初始化交换机和队列,所以在消费者代码中进行配置交换机和队列持久化策略

  • 消息持久化在生产者发送消息的时候

1、交换机持久化

AMQP默认情况下新建的交换机都是持久化的

public class CommonConfig {
    /**
     * km 定义交换机持久化策略
     *
     * @return
     */
    @Bean
    public DirectExchange simpleDirect(){
        //ks 三个参数:交换机名称、是否持久化、当没queue与其绑定的时候是否自动删除
        return new DirectExchange("simple.direct", true, false);
    }
}

2、队列持久化

AMQP默认情况下新建的队列都是持久化的

public class CommonConfig {
    /**
     * km 定义交换机持久化策略
     *
     * @return
     */
    @Bean
    public Queue simpleQueue(){
        //ks 使用QueueBuilder构建队列,durable就是持久化的
        return QueueBuilder.durable("simple.queue").build();
    }
}

3、消息持久化 生产者发送消息的时候设置

SpringAMQP中的消息持久化是默认持久的,可以通过MessageProperties中的DeliveryMode来指定d:

public void testDurableMessage() {
        // ks 1.准备消息
        Message message = MessageBuilder.withBody("hello, spring".getBytes(StandardCharsets.UTF_8)) //kp 消息体,字符集
                .setDeliveryMode(MessageDeliveryMode.PERSISTENT) //kp 持久化
                .build();
        // 2.发送消息
        rabbitTemplate.convertAndSend("simple.queue", message);
    }

3、消费者确认消息

  • 消费者处理消息后可以向MQ发送ack回执,MQ收到ack回执后才会删除该消息

    • 消费者拿到消息后处理完,发送ack回执处理完了,MQ收到回执,就把消息从队列中删除

    • 如果消费者在处理消息中抛出了异常,这时候就会返回nack回执,MQ受到回执就会重新投递一次消息

    • 处理消息的时候服务宕机,什么都没发,MQ就认为消费者废了,等待启动以后再发送给消费者一次,确保消息必须被消费一次

    • 如果消费者消费失败了,会返回unack回执,队列就会再次重试投递给消费者重试,一直到消费者消费了,这样就会导致一直发消息重复,一直重试不停,会导致mq崩溃

  • 而SpringAMQP则允许配置三种确认模式

    • manual:手动ack,需要在业务代码结束后,调用api发送ack

    • auto:自动ack,由spring监测listener代码是否出现异常,没异常则返回ack;抛出异常则返回nack,一般都是用这个

    • none:关闭ack,MQ假定消费者获取消息后会成功处理,因此消息投递后立刻被删除

通过配置设定确认模式

spring:
  rabbitmq:
    listener:
      simple:
        prefetch: 1
        acknowledge-mode: auto #none关闭ack,manual手动ack,auto

4、失败重试机制

  • 当消费者出现异常后,消息会不断requeue重新入队到队列,在重新发送给消费者,然后再次异常,再次requeue,无限循环,导致mq的消息处理飙升,带来不必要的压力、

  • 我们可以利用spring的retry机制,在消费者出现异常时利用本地重试,而不是返回unack进行无限制的requeue到mq队列

    • 当然spring的retry也不是无限重试,可能到一定次数就要停止,重试MQ或者交给人工干预等其他方法

通过配置设定retry

spring:
  rabbitmq:
    listener:
      simple:
        prefetch: 1
        retry: 
            enabled: true #开启消费者失败重试
            initial-interval: 1000 #初试的失败等待时长为1秒
            multiplier: 1 #下次失败的等待时长倍数,下次等待时长 = multiplier * last-interval(上次等待时长)
            max-attempts: 3 #最大重试次数
            stateless: true #默认true无状态,无状态的服务效率高;false有状态。如果业务中包含事务,重试的时候保留事务不会导致事务失效,建议改成false,

4.1、消费者失败消息处理策略

在开启重试模式之后,重试次数耗尽,如果消息依然失败,则需要有MessageRecoverer接口来处理,包含三种不同的实现

  • RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认就是这种方式

  • ImmediateRequeueMessageRevoverer:重试耗尽,返回nack,消息requeue重新入队,有点像没开重试机制,但是消息requeue频率更低了

  • RepublishMeaageRecoverer:重试耗尽,消费者把失败的消息重新投递到指定的交换机,交换机再路由到指定队列里,可以理解为指定交换机和指定队列为失败消息垃圾桶,可以专门指定一个垃圾桶消费者去监听失败消息垃圾桶队列,然后这个消费者可以通知管理员有消息本地重试失败了,然后就可以进行人工处理

Coding

1、在ErrorMessageConfig中,定义接受失败消息的交换机、队列及其绑定关系
@Configuration
public class ErrorMessageConfig {

    /**
     * km 定义异常处理交换机
     *
     * @return
     */
    @Bean
    public DirectExchange errorMessageExchange(){
        return new DirectExchange("error.direct");
    }

    /**
     * km 定义异常处理队列
     *
     * @return
     */
    @Bean
    public Queue errorQueue(){
        return new Queue("error.queue");
    }

    /**
     * km 绑定异常队列和交换机,路由key为error
     *
     * @return
     */
    @Bean
    public Binding errorMessageBinding(){
        return BindingBuilder.bind(errorQueue()).to(errorMessageExchange()).with("error");
    }

    /**
     * km 定义异常消息处理器
     *
     * @param rabbitTemplate 传入模板
     * @return 返回一个
     */
    @Bean
    public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){
        //kp 指定具体的异常处理交换机名称和绑定异常队列的路由key
        return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");
    }
}
2、然后,在ErrorMessageConfig定义RepublishMessageRecoverer
/**
     * km 定义异常消息处理器
     *
     * @param rabbitTemplate 传入模板
     * @return 返回一个
     */
    @Bean
    public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){
        //kp 指定具体的异常处理交换机名称和绑定异常队列的路由key
        return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");
    }

 

posted on 2022-10-13 00:14  老菜农  阅读(277)  评论(0编辑  收藏  举报

导航