RocketMq 重要知識點

OffserStore和信息存儲位置

       實際運行系統,難免會遇到重新消費某條消息,跳過一段時間內的消息等情況。這些異常情況的處理都和offset有關。本節主要分析存儲位置以及如何根據需要調整offset的值。

      首先先來明確一下offset的含義,rocketmq中一種類型的消息會放到一個Topic裏,爲了能夠並行,一般一個Topic會有多個message queue,offset是某個Topic下的一條消息在某個message queue裏的位置,通過offset 的值可以定位到這條消息或者指示Consume從這條消息開始向後繼續處理。

      如下是offset的類結構,主要分爲本地文件類型和broker代存類型兩種。對於DefaultMQPushConsume 來說,默認是集羣模式,也就是同一個消費者組裏的多個消費者每個人消費一部分,各自收到的消息內容就不一樣,這種情況下,由broker端存儲和控制offset的值,使用RomoteBrokerOffsetStore  結構。

備註:(Diagrams可以展示父類,實現類可以用實現implemention或者光標移到該類上右擊Browse Type Hierarchy)

     在廣播模式下,每個消費者都收到這個topic的全部消息,各個消費者之間互不干擾,rocketmq使用LocalFileOffsetStore,把offset存到本地,offsetStroe  使用json格式存儲,簡單明瞭。

    在使用DefaultMQPushConsume 的時候,我們不用關心OffsetStore的事,但是如果是PullConsume,我們就要自己處理OffsetStore了。在上一篇博文中,pullConsume的示例中,代碼把offset存到了內存,沒有持久化存儲,這樣就有可能因爲程序的異常或者重啓而丟失offset,實際應用中並不推薦這麼做。爲了能讓我們看清楚OfsetStore究竟是何物?

1.首先通過廣播模式去消費某條topic 中的消息(LocalFileOffsetStore文件類型是以本地存儲的,必須要是廣播模式下,廣播模式下接受該topic所有的消息,包括歷史消息)

public class ProducerQuickStart {

    public static void main(String[] args) throws MQClientException,InterruptedException {
        DefaultMQProducer producer = new DefaultMQProducer("unique_producer_group__name");
        producer.setInstanceName("instance1");
        producer.setRetryTimesWhenSendFailed(3);//重試次數 192.168.138.47  192.168.142.133  192.168.0.102
        producer.setNamesrvAddr("192.168.139.151:9876");//多個用;分割 192.168.138.47
        producer.start();
        for (int i = 0; i < 1; i++) {
            Date date = new Date();
            SimpleDateFormat sdf = new SimpleDateFormat();
            String format = sdf.format(date);
            System.out.println("準備發送:" + format);
            Message message = new Message("topicName", String.valueOf(i),format.getBytes());
            SendResult sendResult= new SendResult();
            try {
                 sendResult = producer.send(message);
            } catch (RemotingException  |MQBrokerException | InterruptedException e) {
                System.out.println("消息發送失敗:" + sendResult.getMsgId());
                e.printStackTrace();
            }
            System.out.println("key:"+i + "消息的發送結果爲:" + sendResult.toString() + "消息ID爲:" + sendResult.getMsgId());
        }
        producer.shutdown();

    }


}
public class ConsumeQuickStart {

    public static void main(String[] args) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("unique_consume_group_name");
        consumer.setNamesrvAddr("192.168.139.151:9876");//
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
        consumer.setMessageModel(MessageModel.BROADCASTING);//默認是集羣模式
        consumer.subscribe("topicName",null);
        //如果要檢查配置信息
        consumer.registerMessageListener((MessageListenerConcurrently) (listMsg, consumeConcurrentlyContext) -> {
            byte[] body = listMsg.get(0).getBody();
            try {
                String ms = new String(body,"utf-8");
                System.out.println(Thread.currentThread().getName()+"收到消息:" + ms);
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });
        consumer.start();

    }

}

2.  查看LocalFileOffsetStore 本次文件存儲的位置(LocalFileOffsetStore類的源碼)

 public final static String LocalOffsetStoreDir = System.getProperty(
            "rocketmq.client.localOffsetStoreDir",
            System.getProperty("user.home") + File.separator + ".rocketmq_offsets");///Users/humingming/.rocketmq_offsets

this.storePath = LocalOffsetStoreDir + File.separator + //
                this.mQClientFactory.getClientId() + File.separator + //
                this.groupName + File.separator + //
                "offsets.json";//+ 192.168.0.102@DEFAULT/unique_consume_group_name/offsets.json

        我這裏地址就是:/Users/humingming/.rocketmq_offsets/192.168.0.102@DEFAULT/unique_consume_group_name/offset.json

所以可以看出其大概組成就是:用戶根目錄 + .rocketmq_offsets + mq服務端IP地址@? + 消費者組名 + offset.json,因爲我消費端起了兩個服務(之前向兩個服務端發送過兩次消息,所以會有兩個json文件)

{
        "offsetTable":{{
                        "brokerName":"humingmingdeMacBook-Pro.local",
                        "queueId":3,
                        "topic":"topicName"
                }:8,{
                        "brokerName":"humingmingdeMacBook-Pro.local",
                        "queueId":1,
                        "topic":"topicName"
                }:12,{
                        "brokerName":"humingmingdeMacBook-Pro.local",
                        "queueId":2,
                        "topic":"topicName"
                }:8,{
                        "brokerName":"humingmingdeMacBook-Pro.local",
                        "queueId":0,
                        "topic":"topicName"
                }:31
        }
}

// 可以看出當前消費端已經消費 各個隊列最大offset   {key:messagqqueue value:bigOffset}

        值得注意的一點就是:因爲廣播模式下是offset存儲在本地,當有消息發送過來時,此時服務端只要沒收到消息或者消息接受失敗,自然是不會去更改本地存儲的OffsetStore 的json文件的,所以你下一次消息消費時候,會先去看該消息隊列的最大offset,然後在看本地json文件的該消息隊列的value值,如果相同則認爲沒有新的消息,如果隊列的offset > json文件該隊列的value 則認爲有新的消息過來,則會去更新或者新增此文件,所以我們可以主動去更改json文件的value值來讓消費端收不到消息或者重複消費之前消費過的消息。目前據我所知道這僅僅限定於廣播模式。

      那麼問題來了,因爲我們之前有過利用pull模式去拉取消息,但是當時僅僅是將每個隊列的offset存於內存中,比如(這裏是存於map)

package rocketmq.day04;

import com.alibaba.rocketmq.client.consumer.DefaultMQPullConsumer;
import com.alibaba.rocketmq.client.consumer.PullResult;
import com.alibaba.rocketmq.client.exception.MQClientException;
import com.alibaba.rocketmq.common.message.MessageExt;
import com.alibaba.rocketmq.common.message.MessageQueue;

import java.io.UnsupportedEncodingException;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

/**
 * @author heian
 * @create 2020-01-10-11:14 上午
 * @description
 */
public class OffsetPersistence {

    private ConcurrentHashMap<MessageQueue, AtomicLong> offsetTable = new ConcurrentHashMap<>();


    private long getMessageQueueOffset(MessageQueue mq) {
        AtomicLong atomicLong = offsetTable.get(mq);
        if (atomicLong != null)
            return atomicLong.get();
        return 0;
    }

    private void putMessageQueueOffset(MessageQueue mq, long offset) {
        offsetTable.put(mq, new AtomicLong(offset));
    }

    public void pullMsg() throws MQClientException {
        DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("pull_consume_group");
        consumer.setNamesrvAddr("192.168.139.151:9876");// 192.168.0.102
        consumer.start();
        Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("topicName");
        for (int j=0;j<=0;j++){//不斷輪詢  演示的話就調一次算了
            for (MessageQueue mq : mqs) {
                SINGLE_MQ:
                while (true) {
                    try {
                        //第一次拉去將獲得隊列的所有消息寫入到數據庫,保存當時的 nextBeginOffset,
                        //下一次輪詢的時候,在與該隊列比較其nextBeginOffset 如果變化了,則說明有消息進來了,則返回FOUND狀態
                        //我這裏是100 一拉取  假設某條隊列的nextBeginOffset=12 則會去拉去兩次 一次拉取8 狀態爲FOUND,第二次拉取4 狀態爲FOUND  第三次拉去0 狀態爲NO_NEW_MSG
                        PullResult pullResult = consumer.pullBlockIfNotFound(mq, null, getMessageQueueOffset(mq), 100);
                        System.out.println("拉取結果:"+pullResult + "隊列爲:" +mq );
                        putMessageQueueOffset(mq, pullResult.getNextBeginOffset());//可以存在數據庫或者redis中  持久化
                        switch (pullResult.getPullStatus()) {
                            case FOUND:
                                List<MessageExt> listMsg = pullResult.getMsgFoundList();
                                for (int i = 0; i < listMsg.size(); i++) {
                                    byte[] body = listMsg.get(i).getBody();
                                    try {
                                        String ms = new String(body,"utf-8");
                                        //System.out.println("收到消息:" + ms);
                                    } catch (UnsupportedEncodingException e) {
                                        e.printStackTrace();
                                    }
                                }
                                break;
                            case NO_MATCHED_MSG:
                                break;
                            case NO_NEW_MSG:
                                break SINGLE_MQ;
                            case OFFSET_ILLEGAL:
                                break;
                            default:
                                break;
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    public static void main(String[] args) throws MQClientException {
        OffsetPersistence offsetPersistence = new OffsetPersistence();
        offsetPersistence.pullMsg();
    }

}

        那麼我們怎麼將內存中的map持久化到硬盤中呢,其實參考源碼LocalFileOffsetStore類也不難,無非就是將map轉爲字符串,將字符串寫入到file.json中,所以改造下如下:

package rocketmq.day04;

import com.alibaba.rocketmq.client.consumer.DefaultMQPullConsumer;
import com.alibaba.rocketmq.client.consumer.PullResult;
import com.alibaba.rocketmq.client.consumer.store.OffsetSerializeWrapper;
import com.alibaba.rocketmq.client.exception.MQClientException;
import com.alibaba.rocketmq.common.MixAll;
import com.alibaba.rocketmq.common.message.MessageExt;
import com.alibaba.rocketmq.common.message.MessageQueue;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

/**
 * @author heian
 * @create 2020-01-10-11:14 上午
 * @description
 */
public class OffsetPersistence2 {

    private ConcurrentHashMap<MessageQueue, AtomicLong> offsetTable = new ConcurrentHashMap<>();
    //offsetStore 持久化地址   這裏我就寫死了
    private static final String groupName="pull_consume_group";
    private static final String storePath = "/Users/humingming/.rocketmq_offsets/"+groupName+"/offset.json";


    private long getMessageQueueOffset(MessageQueue mq) {
        AtomicLong atomicLong = offsetTable.get(mq);
        if (atomicLong != null)
            return atomicLong.get();
        return 0;
    }

    /**
     * 通過message和offset 得到map  (存在此條消息則put增加,不存在則去set值更新)
     */
    public void putMessageQueueOffset(final MessageQueue mq,long offset){
        if (mq != null){
            AtomicLong offsetOld = this.offsetTable.get(mq);
            if (offsetOld == null){
                this.offsetTable.put(mq,new AtomicLong(offset));
            }else {
                offsetOld.set(offset);
            }
        }
    }

    //將set集合中的消息隊列與公共變量的消息隊列求並集,並存到對應的磁盤中
    public void persistAll(Set<MessageQueue> mqs){
        if (null == mqs || mqs.isEmpty()){
            return;
        }
        OffsetSerializeWrapper offsetSerializeWrapper = new OffsetSerializeWrapper();
        //map中的示例逐個與set集合中比較,有則將map中的消息存入 offsetSerializeWrapper(實際上就是一個可以序列化的map)
        for (Map.Entry<MessageQueue,AtomicLong> entry:this.offsetTable.entrySet()){
            if (mqs.contains(entry.getKey())){
                AtomicLong offset = entry.getValue();
                offsetSerializeWrapper.getOffsetTable().put(entry.getKey(), offset);
            }
        }
        String jsonStr = offsetSerializeWrapper.toJson();
        if (jsonStr != null){
            try {
                MixAll.string2File(jsonStr,this.storePath);
            }catch (IOException e){
                e.printStackTrace();
            }
        }
    }


    public void pullMsg() throws MQClientException {
        DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("pull_consume_group");
        consumer.setNamesrvAddr("192.168.139.151:9876");// 192.168.0.102
        consumer.start();
        Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("topicName");
        for (int j=0;j<=0;j++){
            for (MessageQueue mq : mqs) {
                SINGLE_MQ:
                while (true) {
                    try {
                        //第一次拉去將獲得隊列的所有消息寫入到數據庫,保存當時的 nextBeginOffset,
                        //下一次輪詢的時候,在與該隊列比較其nextBeginOffset 如果變化了,則說明有消息進來了,則返回FOUND狀態
                        //我這裏是100 一拉取  假設某條隊列的nextBeginOffset=12 則會去拉去兩次 一次拉取8 狀態爲FOUND,第二次拉取4 狀態爲FOUND  第三次拉去0 狀態爲NO_NEW_MSG
                        PullResult pullResult = consumer.pullBlockIfNotFound(mq, null, getMessageQueueOffset(mq), 100);
                        System.out.println("拉取結果:"+pullResult + "隊列爲:" +mq );
                        putMessageQueueOffset(mq, pullResult.getNextBeginOffset());//將隊列信息存於map
                        switch (pullResult.getPullStatus()) {
                            case FOUND:
                                List<MessageExt> listMsg = pullResult.getMsgFoundList();
                                for (int i = 0; i < listMsg.size(); i++) {
                                    byte[] body = listMsg.get(i).getBody();
                                    try {
                                        String ms = new String(body,"utf-8");
                                        //System.out.println("收到消息:" + ms);
                                    } catch (UnsupportedEncodingException e) {
                                        e.printStackTrace();
                                    }
                                }
                                break;
                            case NO_MATCHED_MSG:
                                break;
                            case NO_NEW_MSG:
                                break SINGLE_MQ;
                            case OFFSET_ILLEGAL:
                                break;
                            default:
                                break;
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
            //持久化
            persistAll(mqs);
        }
    }



    public static void main(String[] args) throws MQClientException {
        OffsetPersistence2 offsetPersistence = new OffsetPersistence2();
        offsetPersistence.pullMsg();
    }

}

 所以會在/Users/humingming/.rocketmq_offsets/pull_consume_group 生成一個json文件 如下: 

控制檯打印信息如下:

拉取結果:PullResult [pullStatus=FOUND, nextBeginOffset=8, minOffset=0, maxOffset=8, msgFoundList=8]隊列爲:MessageQueue [topic=topicName, brokerName=bogon, queueId=2]
拉取結果:PullResult [pullStatus=NO_NEW_MSG, nextBeginOffset=8, minOffset=0, maxOffset=8, msgFoundList=0]隊列爲:MessageQueue [topic=topicName, brokerName=bogon, queueId=2]
拉取結果:PullResult [pullStatus=FOUND, nextBeginOffset=12, minOffset=0, maxOffset=12, msgFoundList=12]隊列爲:MessageQueue [topic=topicName, brokerName=bogon, queueId=1]
拉取結果:PullResult [pullStatus=NO_NEW_MSG, nextBeginOffset=12, minOffset=0, maxOffset=12, msgFoundList=0]隊列爲:MessageQueue [topic=topicName, brokerName=bogon, queueId=1]
拉取結果:PullResult [pullStatus=FOUND, nextBeginOffset=32, minOffset=0, maxOffset=38, msgFoundList=32]隊列爲:MessageQueue [topic=topicName, brokerName=bogon, queueId=0]
拉取結果:PullResult [pullStatus=FOUND, nextBeginOffset=38, minOffset=0, maxOffset=38, msgFoundList=6]隊列爲:MessageQueue [topic=topicName, brokerName=bogon, queueId=0]
拉取結果:PullResult [pullStatus=NO_NEW_MSG, nextBeginOffset=38, minOffset=0, maxOffset=38, msgFoundList=0]隊列爲:MessageQueue [topic=topicName, brokerName=bogon, queueId=0]
拉取結果:PullResult [pullStatus=FOUND, nextBeginOffset=8, minOffset=0, maxOffset=8, msgFoundList=8]隊列爲:MessageQueue [topic=topicName, brokerName=bogon, queueId=3]
拉取結果:PullResult [pullStatus=NO_NEW_MSG, nextBeginOffset=8, minOffset=0, maxOffset=8, msgFoundList=0]隊列爲:MessageQueue [topic=topicName, brokerName=bogon, queueId=3]

 

 

發佈了78 篇原創文章 · 獲贊 18 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章