本篇詳細介紹消息發送、消息消費、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 等中間件有深入研究。歡迎加入筆者的知識星球,一起探討高併發、分佈式服務架構,分享閱讀源碼心得。