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)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章