RabbitMQ消息可靠性、延时队列

消息可靠性

生产者消息确认

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机制,不过是定义ReturnCallback
  • template.mandatory:定义消息路由失败时的策略。true,则调用ReturnCallbackfalse:则直接丢弃消息

定义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时任意一个到期就会成为死信

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值