RocketMQ msgId與offsetMsgId釋疑(實戰篇)

本篇詳細介紹消息發送、消息消費、RocketMQ queryMsgById 命令以及 rocketmq-console 等使用場景中究竟是用的哪一個ID。

1、拋出問題

1.1 從消息發送看消息ID

package org.apache.rocketmq.example.quickstart;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.common.RemotingHelper;
public class Producer {
    public static void main(String[] args)  {
        try {
            DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
            producer.setNamesrvAddr("127.0.0.1:9876");
            producer.start();
            Message msg = new Message("TestTopic" /* Topic */,null /* Tag */, ("Hello RocketMQ test1" ).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */);
            SendResult sendResult = producer.send(msg);
            System.out.printf("%s%n", sendResult);
            producer.shutdown();
        } catch (Throwable e) {
            e.printStackTrace();
        }

    }
}

執行效果如圖所示:
在這裏插入圖片描述即消息發送會返回 msgId 與 offsetMsgId。

1.2 從消息消費看消息ID

package org.apache.rocketmq.example.quickstart;
import java.util.List;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.message.MessageExt;
public class Consumer {
    public static void main(String[] args) throws InterruptedException, MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_1");
        consumer.setNamesrvAddr("127.0.0.1:9876");
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
        consumer.subscribe("TestTopic", "*");
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                ConsumeConcurrentlyContext context) {
                System.out.println("MessageExt msg.getMsgId():" +  msgs.get(0).getMsgId());
                System.out.println("-------------------分割線-----------------");
                System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
        System.out.printf("Consumer Started.%n");
    }
}

執行效果如圖所示:
在這裏插入圖片描述
不知道大家是否有注意到,調用 msgs.get(0).getMsgId()返回的msgId 與直接輸出msgs中的 msgId 不一樣,那這又是爲什麼呢?答案在本文的第二部分有詳細分析。

2、消息ID釋疑

從消息發送的結果可以得知,RocketMQ 發送的返回結果會返回msgId 與 offsetMsgId,那這兩個 msgId 分別是代表什麼呢?

  • msgId:該ID 是消息發送者在消息發送時會首先在客戶端生成,全局唯一,在 RocketMQ 中該 ID 還有另外的一個叫法:uniqId,無不體現其全局唯一性。
  • offsetMsgId:消息偏移ID,該 ID 記錄了消息所在集羣的物理地址,主要包含所存儲 Broker 服務器的地址( IP 與端口號)以及所在commitlog 文件的物理偏移量。

2.1 msgId 即全局唯一 ID 構建規則

在這裏插入圖片描述
從這張圖可以看出,msgId確實是客戶端生成的,接下來我們詳細分析一下其生成算法。

MessageClientIDSetter#createUniqID

public static String createUniqID() {
    StringBuilder sb = new StringBuilder(LEN * 2);
    sb.append(FIX_STRING);    // @1
    sb.append(UtilAll.bytes2string(createUniqIDBuffer()));  // @2
    return sb.toString();
}

一個 uniqID 的構建主要分成兩個部分:FIX_STRING 與唯一 ID 生成算法,顧名思義,FIX_STRING 就是一個客戶端固定一個前綴,那接下來先看一下固定字符串的生成規則。

2.1.1 FIX_STRING

MessageClientIDSetter靜態代碼塊

static {
    byte[] ip;
    try {
        ip = UtilAll.getIP();
    } catch (Exception e) {
        ip = createFakeIP();
    }
    LEN = ip.length + 2 + 4 + 4 + 2;
    ByteBuffer tempBuffer = ByteBuffer.allocate(ip.length + 2 + 4);
    tempBuffer.position(0);
    tempBuffer.put(ip);
    tempBuffer.position(ip.length);
    tempBuffer.putInt(UtilAll.getPid());
    tempBuffer.position(ip.length + 2);
    tempBuffer.putInt(MessageClientIDSetter.class.getClassLoader().hashCode());
    FIX_STRING = UtilAll.bytes2string(tempBuffer.array());
    setStartTime(System.currentTimeMillis());
    COUNTER = new AtomicInteger(0);
}

從這裏可以看出 FIX_STRING 的主要由:客戶端的IP、進程ID、加載 MessageClientIDSetter 的類加載器的 hashcode。

2.1.2 唯一性算法

msgId 的唯一性算法由 MessageClientIDSetter 的createUniqIDBuffer 方法實現。

private static byte[] createUniqIDBuffer() {
    ByteBuffer buffer = ByteBuffer.allocate(4 + 2);
    long current = System.currentTimeMillis();
    if (current >= nextStartTime) {
        setStartTime(current);
    }
    buffer.position(0);
    buffer.putInt((int) (System.currentTimeMillis() - startTime));
    buffer.putShort((short) COUNTER.getAndIncrement());
    return buffer.array();
}

可以得出 msgId 的後半段主要由:當前時間與系統啓動時間的差值,以及自增序號。

2.2 offsetMsgId構建規則

在這裏插入圖片描述
在消息 Broker 服務端將消息追加到內存後會返回其物理偏移量,即在 commitlog 文件中的文件,然後會再次生成一個id,代碼中雖然也叫 msgId,其實這裏就是我們常說的 offsetMsgId,即記錄了消息的物理偏移量,故我們重點來看一下其具體生成規則:
MessageDecoder#createMessageId

public static String createMessageId(final ByteBuffer input ,
            final ByteBuffer addr, final long offset) {
	input.flip();
    int msgIDLength = addr.limit() == 8 ? 16 : 28;
    input.limit(msgIDLength);
    input.put(addr);
    input.putLong(offset);
    return UtilAll.bytes2string(input.array());
}

首先結合該方法的調用上下文,先解釋一下該方法三個入參的含義:

  • ByteBuffer input
    用來存放 offsetMsgId 的字節緩存區( NIO 相關的基礎知識)
  • ByteBuffer addr
    當前 Broker 服務器的 IP 地址與端口號,即通過解析 offsetMsgId 從而得到消息服務器的地址信息。
  • long offset
    消息的物理偏移量。
    即構成 offsetMsgId 的組成部分:Broker 服務器的 IP 與端口號、消息的物理偏移量。

溫馨提示:即在 RocketMQ中,只需要提供 offsetMsgId,可用不必知道該消息所屬的topic信息即可查詢該條消息的內容。

2.3 消息發送與消息消費返回的消息ID信息

消息發送時會在 SendSesult中返回 msgId、offsetMsgId,在瞭解了這個兩個 ID 的含義時則問題不大,接下來重點介紹一下消息消費時返回的 msgId 到底是哪一個。

在消息消費時,我們更加希望因爲 msgId (即客戶端生成的全局唯一性ID),因爲該全局性 ID 非常方便實現消費端的冪等。

在本文的1.2節我們也提到一個現象,爲什麼如下圖代碼中輸出的 msgId 會不一樣呢?
在這裏插入圖片描述
在客戶端返回的 msg 信息,其最終返回的對象是 MessageClientExt ,繼承自 MessageExt。
那我們接下來分別看一下其 getMsgId() 方法與 toString 方法即可。

@Override
public String getMsgId() {
    String uniqID = MessageClientIDSetter.getUniqID(this);
    if (uniqID == null) {
        return this.getOffsetMsgId();
    } else {
        return uniqID;
    }
}

原來在調用 MessageClientExt 中的 getMsgId 方法時,如果消息的屬性中存在其唯一ID,則返回消息的全局唯一ID,否則返回消息的 offsetMsgId。

而 MessageClientExt 方法並沒有重寫 MessageExt 的 toString 方法,其實現如圖所示:
在這裏插入圖片描述
故返回的是 MessageExt中 的 msgId,該 msgId 存放的是offsetMsgId,所以才造成了困擾。

溫馨提示:如果消息消費失敗需要重試,RocketMQ 的做法是將消息重新發送到 Broker 服務器,此時全局 msgId 是不會發送變化的,但該消息的 offsetMsgId 會發送變化,因爲其存儲在服務器中的位置發生了變化。

3、實踐經驗

在回答了消息發送與消息消費關於msgId與offsetMsgId的困擾後,再來介紹一下如果根據msgId去查詢消息。

想必大家對 rocketmq-console ,那在消息查找界面,展示的消息列表中返回的 msgId 又是哪一個呢?
在這裏插入圖片描述
這裏的 Message ID 返回的是消息的全局唯一ID。

其實 RokcetMQ 也提供了 queryMsgById 命令來查看消息的內容,不過這裏的 msgId 是 offsetMsgId,我們首先將全局唯一ID傳入命令,其執行效果如下:
在這裏插入圖片描述
發現報錯,那我們將 offsetMsgId 傳入其執行效果如圖所示:
在這裏插入圖片描述
但在 rocketmq-console 的根據消息ID去查找消息,無論傳入哪個msgId,下圖該功能都能返回正確的結果:
在這裏插入圖片描述
這是因爲 rocketmq-console 做了兼容,首先將傳入的 msgId 用 queryMsgById 該命令去查,如果報錯,則當成 uniqID(全局ID)去查,首先全局ID會存儲在消息的屬性中,並會創建 Hash 索引,即可用通過 indexfile 快速定位到該條消息。

關於 RocketMQ 消息ID的相關問題就介紹到這裏了,希望能得到您的認可,幫忙點個贊,謝謝。


作者信息:丁威,《RocketMQ技術內幕》作者、CSDN博客專家,原創公衆號『中間件興趣圈』維護者。擅長JAVA編程,對主流中間件 RocketMQ、Dubbo、ElasticJob、Netty、Sentinel、Mybatis、Mycat 等中間件有深入研究。歡迎加入筆者的知識星球,一起探討高併發、分佈式服務架構,分享閱讀源碼心得。

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