使用RocketMQ如何處理重複消息

當我們在使用RocketMQ發送消息時,爲了實現百分百消息可靠投遞,那麼重複消息就不可避免。

發送消息(同步方式)一般經過三個步驟:
首先:將消息從生產端發送到broker,生產端繼續等待這條消息的處理結果(broker是否能夠正常接收);

然後:broker做一些處理(比如同步到從節點以及持久化等);

最後:將應答返還給客戶端,客戶端收到該消息的應答後,做下一步處理。

我們考慮一下這樣的情況:

a. 客戶端將一條消息發送(同步方式)到broker,broker成功接收,當broker將成功接收的結果返回給客戶端時,這個確認消息在網絡傳輸的過程中,由於某些原因丟失了;

b. 那麼對於客戶端來說,爲了實現高可用機制,在超時後默認有2次重試機會,將這條消息會再次發送給broker;

c. 當broker處理完這條消息後,將成功接收的結果返回給客戶端,這次客戶端成功的接收到了來自broker發來的確認消息,然後進行下一步的處理;

此時,broker就有兩條重複的消息。

RocketMQ爲了簡化功能上的設計,允許重複消息的存在,將消息去重的任務交給我們自己去處理。在我公司,我的做法如下:

a. 在消息處理之前,先把這條消息的訂單號放入redis中(有過期時間),即代表這條消息正在處理中,不允許在同一時間處理兩條相同的消息;

b. 根據訂單號,在消息消費記錄表中查詢是否存在(兜底方案,當redis失效時);

    b1. 若存在且處理狀態爲已成功處理,則返回處理成功並把redis中訂單號刪除;

    b2. 若存在但處理狀態爲處理失敗,則消費這條消息;

          b2.1 若這條消息消費成功:

                  b2.1.1 在消息消費記錄表中的狀態置爲已成功處理;

                  b2.1.2 將redis中的訂單號刪除;

                   b2.1.3 返回處理成功;

          b2.2 若這條消息處理失敗:

                  b2.2.1 在消息消費記錄表中的狀態置爲處理失敗;

                  b2.2.2 將redis中的訂單號刪除;

                  b2.2.3 返回稍後重試;

    b3. 若不存在,則:

          b3.1 將這條消息插入消費記錄表中,處理狀態置爲處理中;

          b3.2 消費這條消息,若消費成功,則走 b2.1,若消費失敗,則走 b2.2

僞代碼如下:

public class PkgConcurrentlyListener implements MessageListenerConcurrently {
    protected static Logger logger = LoggerFactory.getLogger(PkgConcurrentlyListener.class);
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
        MessageExt me = list.get(0);
        String topic = me.getTopic();
        String tags = me.getTags();
        String keys = me.getKeys();
        boolean result = getLock30Second(keys,keys);
        if(!result){
            //正在處理中,請稍後再試
            return ConsumeConcurrentlyStatus.RECONSUME_LATER;
        }
        try {
            String msgBody = new String(me.getBody(), RemotingHelper.DEFAULT_CHARSET);
            logger.info("消費者消費消息 topic = 【" + topic + "】keys = 【" + keys + "】,msgBody = 【" + msgBody + "】");
            
            //根據keys在消費記錄表中查詢數據
            
            
            //業務的處理邏輯

            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        } catch (Exception e) {
            e.printStackTrace();
            return ConsumeConcurrentlyStatus.RECONSUME_LATER;//消息處理失敗,過會再試
        } finally {
            //無論消費成功與否,都刪除
            RedisPoolUtil.del(keys);
        }
    }
    /**
     * 鎖住某個key值30S,需要解鎖時刪除即可
     * 注意:不要使用這種方式,因爲這種做法不能防死鎖,使用這篇文章    
     * https://blog.csdn.net/zhaoming19870124/article/details/91041855介紹的優化版本
     */
    public static boolean getLock30Second(final String key, final String value) {
        Jedis jedis = RedisPoolConfig.getJedis();
        try {
            Long flag = jedis.setnx(key, value);
            System.out.println("將key = 【" + key + "】,放入redis中的結果 = 【" + flag + "】");
            if (1 == flag) {
                //如果在redis中不存在,則設置有效期,防止永久存在;
                //當這個keys永久存在時,若一條消息在第一次消費時,消費失敗了,則永遠不會被消費了。
                // 因爲接下來的每次消費,都被redis攔下來了
                flag = jedis.expire(key, 30);
            }
            return flag == 1L ? true : false;
        } catch (Exception e) {
            System.out.println(e);
            return false;
        }
    }
}

 

 

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