Springboot+RabbitMQ实现消息确认、防止消息重复消费、延时队列

学习RabbitMQ也有一段时间了,特记录下学习心得。

RabbitMQ现阶段应用场景包括:

1:订单生成后,通过RabbitMQ推送短信和邮件。

2:订单生成后,一小时内未支付则关闭订单。

由应用场景引发的问题:

1:如何确保消费端成功消费消息

2:如何防止消费端重复消费消息

3:由TTL和DLX特性实现的延时队列第一条消息延时时长若比后一条消息延时时长更久,会导致后一条消息无法准时消费(消息在同一队列里)

1:消息确认

RabbitMQ消息确认又分为生产端确认和消费端确认

生产端确认:

首先需要确保创建的Exchange和Queue持久化(新建时默认持久化)

@Bean
public TopicExchange createTopicExchange(){
    return new TopicExchange(TOPIC_EXCHANGE);
}
@Bean
public Queue createTopicQueue(){
    return new Queue(T_TOPIC_QUEUE);
}

可通过RabbitMQ后台进行查看

在这里插入图片描述

然后在application.yml中配置

spring.rabbitmq.publisher-confirms: true
spring.rabbitmq.publisher-returns: true

实现RabbitTemplate.ConfirmCallbackRabbit和RabbitTemplate.ReturnCallback两个接口

@Component
public class RabbitMqProducer implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnCallback {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    //需初始化设置
    @Bean
    public void createRabbitTemplate() {
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnCallback(this);
    }

    //确认消息是否到达Exchange b为false则发送失败
    @Override
    public void confirm(CorrelationData correlationData, boolean b, String s) {
        if (!b) {
            System.out.println("消息发送失败,失败原因:" + s);
        }
    }

    //确认消息是否到达Queue,消息发送失败触发(比如routing_key匹配不到queue)
    @Override
    public void returnedMessage(Message message, int i, String s, String s1, String s2) {
        System.out.println("进入returnCallback!exchange:" + s1 + ",routingkey:" + s2);
        System.out.println("消息内容:" + message + ",失败原因:" + s);
    }
}

消费端确认

消费者确认模式默认为自动确认模式,因此需要把消费者确认模式改为手动模式

在application.yml中配置

spring.rabbitmq.listener.simple.acknowledge-mode: manual

若成功消费,则调用channel.basicAck()确认消费

参数说明:
deliveryTag:该消息的index
multiple:是否批量.true:将一次性ack所有小于deliveryTag的消息。

@RabbitListener(queues = RabbitMqConfig.T_TOPIC_QUEUE)
public void processMessage(Message message, Channel channel) throws IOException {
    System.out.println(LocalDateTime.now()+ "消费端收到消息:" + new String(message.getBody()));
    channel.basicAck(message.getMessageProperties().getDeliveryTag(),true);
}

若消费失败,即执行过程中出现异常则调用channel.basicNack()
注意requeue参数需要为false,若为true时会导致重发->异常->重发死循环,此时的做法应该是确认消息已消费,记录异常信息,把消息转发到死信队列中,等待后续处理。

参数说明:
deliveryTag:该消息的index
multiple:是否批量.true:将一次性拒绝所有小于deliveryTag的消息。
requeue:被拒绝的是否重新入队列

@RabbitListener(queues = RabbitMqConfig.T_TOPIC_QUEUE)
public void processM(Message message, Channel channel) throws Exception {
    System.out.println(LocalDateTime.now()+"消费端收到消息:" + new String(message.getBody()));
    //模拟出现异常
    try {
        if (true) {
            throw new IOException("抛异常了!");
        }
    } catch (Exception e) {
        channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
    }
}

2:防止消息重复消费

防止消息重复消费就是保证接口的幂等性,也就是保证相同的数据多次请求同一接口结果仍然一致。
比较通用的做法是给消息设置一个全局性唯一性Id(可根据Snowflake算法实现)存储在redis中,请求接口前先查询redis判断再执行后续操作。
根据具体的业务进行分析,例如订单扣款,请求接口前先根据订单表的是否支付相关字段进行判断等等

3:延时队列

由RabbitMQ自带的TTL和DLX特性虽可实现延时队列,但第一条消息延时时长若比后一条消息延时时长更久的话,会导致后一条消息无法准时消费。
解决方法:
通过:https://www.rabbitmq.com/community-plugins.html ,下载rabbitmq_delayed_message_exchange插件,然后解压放置到RabbitMQ安装目录下的plugins目录。(注意插件版本要和RabbitMQ的版本一致,RabbitMQ版本可通过rabbitmqctl status命令查看,也可以直接在RabbitMQ后台查看)
进入安装目录下的sbin目录执行下面命令让该插件生效,重启RabbitMQ后生效。

rabbitmq-plugins enable rabbitmq_delayed_message_exchange

使用CustomExchange进行初始化Exchange相关属性,其它和之前一样配置

 //利用rabbitmq_delayed_message_exchange插件实现延时队列
    @Bean
    public Queue createCustomQueue(){
        return new Queue(TOPIC_CUSTOM_QUEUE);
    }
    @Bean
    public CustomExchange createCustomExchange(){
        Map<String, Object> args = new HashMap<>();
        args.put("x-delayed-type", "topic");
        return new CustomExchange(TOPIC_CUSTOM_EXCHANGE, "x-delayed-message", true, false, args);
    }

    @Bean
    public Binding bingdelayCustomTopic(){
        return BindingBuilder.bind(createCustomQueue()).to(createCustomExchange()).with(TOPIC_ROUT_KEY).noargs();
    }

发送端通过自定义时长来实现延时队列

public boolean sendCustomMg(String message,int time){
    System.out.println("生产者custom发送:"+ LocalDateTime.now()+":"+message);
    rabbitTemplate.convertAndSend(RabbitMqConfig.TOPIC_CUSTOM_EXCHANGE,"topic.mg",message,
            delaymessage->{delaymessage.getMessageProperties().setDelay(time*1000);return delaymessage;});
    return true;
}

注意区别

TTL

delaymessage.getMessageProperties().setExpiration(time*1000+"")

rabbitmq_delayed_message_exchange插件

delaymessage.getMessageProperties().setDelay(time*1000)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章