RocketMQ重試機制和消息冪等


一、重試機制

  由於MQ經常處於復雜的分布式系統中,考慮網絡波動,服務宕機,程序異常因素,很有可能出現消息發送或者消費失敗的問題。因此,消息的重試就是所有MQ中間件必須考慮到的一個關鍵點。如果沒有消息重試,就可能產生消息丟失的問題,可能對系統產生很大的影響。所以,秉承寧可多發消息,也不可丟失消息的原則,大部分MQ都對消息重試提供了很好的支持。

  MQ 消費者的消費邏輯失敗時,可以通過設置返回狀態達到消息重試的結果。

  MQ 消息重試只針對集群消費方式生效;廣播方式不提供失敗重試特性,即消費失敗后,失敗消息不再重試,繼續消費新的消息。

1、模擬異常

消費者:

package com.zn.retry;

import com.alibaba.rocketmq.client.consumer.DefaultMQPushConsumer;
import com.alibaba.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import com.alibaba.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import com.alibaba.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import com.alibaba.rocketmq.client.exception.MQClientException;
import com.alibaba.rocketmq.common.message.MessageExt;

import java.util.List;

/**
 * RocketMQ重試機制消費者
 */
public class RetryConsumer {
    public static void main(String[] args) throws MQClientException {
        //創建消費者
        DefaultMQPushConsumer consumer=new DefaultMQPushConsumer("rmq-group");
        //設置NameServer地址
        consumer.setNamesrvAddr("192.168.33.135:9876;192.168.33.136:9876");
        //設置實例名稱
        consumer.setInstanceName("consumer");
        //訂閱topic
        consumer.subscribe("itmayiedu-topic","TagA");

        //監聽消息
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                //獲取消息
                for (MessageExt messageExt:list){
                    System.out.println(messageExt.getMsgId()+"---"+new String(messageExt.getBody()));
                }
                try {
                    //模擬錯誤
                    int i=5/0;
                }catch (Exception e){
                    e.printStackTrace();
                    //需要重試
                    return ConsumeConcurrentlyStatus.RECONSUME_LATER;
                }
                //不需要重試
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        //啟動消費者
        consumer.start();
        System.out.println("Consumer Started!");
    }
}

控制台效果:

  

2、模擬網絡延遲

package com.zn.retry;

import com.alibaba.rocketmq.client.consumer.DefaultMQPushConsumer;
import com.alibaba.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import com.alibaba.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import com.alibaba.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import com.alibaba.rocketmq.client.exception.MQClientException;
import com.alibaba.rocketmq.common.message.MessageExt;

import java.util.List;

/**
 * RocketMQ重試機制消費者
 */
public class RetryConsumer {
    public static void main(String[] args) throws MQClientException {
        //創建消費者
        DefaultMQPushConsumer consumer=new DefaultMQPushConsumer("rmq-group");
        //設置NameServer地址
        consumer.setNamesrvAddr("192.168.33.135:9876;192.168.33.136:9876");
        //設置實例名稱
        consumer.setInstanceName("consumer");
        //訂閱topic
        consumer.subscribe("itmayiedu-topic","TagA");

        //監聽消息
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                //獲取消息
                for (MessageExt messageExt:list){
                    System.out.println(messageExt.getMsgId()+"---"+new String(messageExt.getBody()));
                }
                try {
                    //網絡延遲
                    Thread.sleep(600000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //消費成功
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        //啟動消費者
        consumer.start();
        System.out.println("Consumer Started!");
    }
}

二、消息冪等

1、在什么情況下會發生RocketMQ的消息重復消費

   ①、當系統的調用鏈路比較長的時候,比如系統A調用系統B,系統B再把消息發送到RocketMQ中,在系統A調用系統B的時候,如果系統B處理成功,但是遲遲沒有將調用成功的結果返回給系統A的時候,系統A就會嘗試重新發起請求給系統B,造成系統B重復處理,發起多條消息給RocketMQ造成重復消費;

  ②、在系統B發送給RocketMQ的時候,也有可能會發生和上面一樣的問題,消息發送超時,節骨系統B重試,導致RocketMQ接收到了重讀消息;

  ③、當RocketMQ成功接收到消息,並將消息交給消費者處理,如果消費者消費完成后還沒來得及提交offset給RocketMQ,自己宕機或者重啟了,那么RocketMQ沒有接收到offset,就會認為消費失敗了,會重發消息給消費者再次消費;

2、如何解決消息的重復消費

  通過冪等性來保證,只要保證重復消息不對結果產生影響,就完美地解決這個問題。

在生產者端保證冪等性,一下兩種方式:

  ①、RocketMQ支持消息查詢的功能,只要去RocketMQ查詢一下是否已經發送過該條消息就可以了,不存在則發送,存在則不發送;

  ②、引入Redis,在發送消息到RocketMQ成功之后,向Redis中插入一條數據,如果發送重試,則先去Redis查詢一個該條消息是否已經發送過了,存在的話就不重復發送消息了;

  方法一:RocketMQ消息查詢的性能不是特別好,如果在高並發的場景下,每條消息在發送到RocketMQ時都去查詢一下,可能會影響接口的性能;

  方法二:在一些極端的場景下,Redis也無法保證消息發送成功之后,就一定能寫入Redis成功,比如寫入消息成功而Redis此時宕機,那么再次查詢Redis判斷消息是否已經發送過,是無法得到正確結果的;

3、生產者

package com.zn.idempotent;

import com.alibaba.rocketmq.client.exception.MQBrokerException;
import com.alibaba.rocketmq.client.exception.MQClientException;
import com.alibaba.rocketmq.client.producer.DefaultMQProducer;
import com.alibaba.rocketmq.client.producer.SendResult;
import com.alibaba.rocketmq.common.message.Message;
import com.alibaba.rocketmq.remoting.exception.RemotingException;

/**
 * 消息冪等生產者
 */
public class IdempotentProvider {
    public static void main(String[] args) throws MQClientException, InterruptedException, RemotingException, MQBrokerException {
        //創建一個生產者
        DefaultMQProducer producer=new DefaultMQProducer("rmq-group");
        //設置NameServer地址
        producer.setNamesrvAddr("192.168.33.135:9876;192.168.33.136:9876");
        //設置生產者實例名稱
        producer.setInstanceName("producer");
        //啟動生產者
        producer.start();

            //發送消息
            for (int i=1;i<=1;i++){
                //模擬網絡延遲,每秒發送一次MQ
                Thread.sleep(1000);
                //創建消息,topic主題名稱  tags臨時值代表小分類, body代表消息體
                Message message=new Message("itmayiedu-topic03","TagA",("itmayiedu-"+i).getBytes());
                //消息的唯一標識
                message.setKeys("訂單消息:"+i);
                //發送消息
                SendResult sendResult=producer.send(message);
                System.out.println("信息冪等問題來了:"+sendResult.toString());
            }
        producer.shutdown();
    }
}

4、消費者

package com.zn.idempotent;

import com.alibaba.rocketmq.client.consumer.DefaultMQPushConsumer;
import com.alibaba.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import com.alibaba.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import com.alibaba.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import com.alibaba.rocketmq.client.exception.MQClientException;
import com.alibaba.rocketmq.common.message.MessageExt;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.LogManager;
import java.util.logging.Logger;

/**
 * 消息冪等消費者
 */
public class IdempotentConsumer {

    static private Map<String, Object> logMap = new HashMap<>();

    public static void main(String[] args) throws MQClientException {
        //創建消費者
        DefaultMQPushConsumer consumer=new DefaultMQPushConsumer("rmq-group");
        //設置NameServer地址
        consumer.setNamesrvAddr("192.168.33.135:9876;192.168.33.136:9876");
        //設置實例名稱
        consumer.setInstanceName("consumer");
        //訂閱topic
        consumer.subscribe("itmayiedu-topic03","TagA");

        //監聽消息
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                String key=null;
                String msgId=null;

                    for (MessageExt messageExt:list){
                        key=messageExt.getKeys();
                        //判讀redis中有沒有當前消息key
                        if (logMap.containsKey(key)) {
                            // 無需繼續重試。
                            System.out.println("key:"+key+",已經消費,無需重試...");
                            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                        }
                        //RocketMQ由於是集群環境,所以產生的消息ID可能會重復
                        msgId = messageExt.getMsgId();
                        System.out.println("key:" + key + ",msgid:" + msgId + "---" + new String(messageExt.getBody()));
                        //將當前key保存在redis中
                        logMap.put(messageExt.getKeys(),messageExt);
                    }
                try {
                    int i=5/0;
                }catch (Exception e){
                    e.printStackTrace();
                    //人工補償
                    return ConsumeConcurrentlyStatus.RECONSUME_LATER;
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        }
    });
        //啟動消費者
        consumer.start();
        System.out.println("Consumer Started!");
    }
}

5、控制台效果

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM