public static void main(String[] args) throws Exception {
// Instantiate with a producer group name.
DefaultMQProducer producer = new
DefaultMQProducer("GroupNameDemo");
// Specify name server addresses.
producer.setNamesrvAddr("localhost:9876");
//Launch the instance.
producer.start();
for (int i = 0; i < 100; i++) {
//Create a message instance, specifying topic, tag and message body.
Message msg = new Message("TopicDemo" /* Topic */,
"TagA" /* Tag */,
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
);
// Call send message to deliver message to one of brokers.
SendResult sendResult = producer.send(msg);
System.out.printf("%s%n", sendResult);
}
//Shut down once the producer instance is not longer in use.
producer.shutdown();
}
訊息傳送的入口DefaultMQProducer#send()
// DefaultMQProducer.java
@Override
public SendResult send(
Message msg) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
// 檢查訊息
Validators.checkMessage(msg, this);
msg.setTopic(withNamespace(msg.getTopic()));
// 傳送訊息
return this.defaultMQProducerImpl.send(msg);
}
訊息檢驗
- 檢查訊息內容不能為空
- 檢查訊息長度不能為0
- 預設情況,訊息大小不能超過 4M
// Validators.java
public static void checkMessage(Message msg, DefaultMQProducer defaultMQProducer)
throws MQClientException {
if (null == msg) {
// 拋異常
}
// topic
Validators.checkTopic(msg.getTopic());
// body 檢查訊息內容不能為空
if (null == msg.getBody()) {
// 拋異常
}
// 檢查訊息長度不能為0
if (0 == msg.getBody().length) {
// 拋異常
}
// 預設情況,訊息大小不能超過 4M = 1024 * 1024 * 4;
if (msg.getBody().length > defaultMQProducer.getMaxMessageSize()) {
// 拋異常
}
}
訊息傳送
// DefaultMQProducerImpl.java
// DEFAULT SYNC
public SendResult send(Message msg) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
// this.defaultMQProducer.getSendMsgTimeout() 獲取訊息傳送超時時間, 預設為3s
return send(msg, this.defaultMQProducer.getSendMsgTimeout());
}
public SendResult send(Message msg,long timeout) throws MQClientException,RemotingException, MQBrokerException, InterruptedException {
return this.sendDefaultImpl(msg, CommunicationMode.SYNC, null, timeout);
}
private SendResult sendDefaultImpl(
Message msg,
final CommunicationMode communicationMode,
final SendCallback sendCallback,
final long timeout
) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
this.makeSureStateOK();
Validators.checkMessage(msg, this.defaultMQProducer);
final long invokeID = random.nextLong();
long beginTimestampFirst = System.currentTimeMillis();
long beginTimestampPrev = beginTimestampFirst;
long endTimestamp = beginTimestampFirst;
// 這裡去查詢主題路由資訊
TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
// 省略......
}
查詢主題路由資訊
如果生產者已經快取了topic的路由資訊,則直接返回。如果沒有快取,則向NameServer查詢該topic的路由資訊。
如果最終未能查詢到路由資訊,則直接丟擲異常。
// DefaultMQProducerImpl.java
private TopicPublishInfo tryToFindTopicPublishInfo(final String topic) {
// 1. 先從快取中獲取
TopicPublishInfo topicPublishInfo = this.topicPublishInfoTable.get(topic);
if (null == topicPublishInfo || !topicPublishInfo.ok()) {
this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo());
// 2. 向NameServer查詢該topic的路由資訊
this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic);
topicPublishInfo = this.topicPublishInfoTable.get(topic);
}
if (topicPublishInfo.isHaveTopicRouterInfo() || topicPublishInfo.ok()) {
return topicPublishInfo;
} else {
this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic, true, this.defaultMQProducer);
topicPublishInfo = this.topicPublishInfoTable.get(topic);
return topicPublishInfo;
}
}
TopicPublishInfo
資訊如下
public class TopicPublishInfo {
// 是否是順序訊息
private boolean orderTopic = false;
private boolean haveTopicRouterInfo = false;
// 該主題佇列的訊息佇列
private List<MessageQueue> messageQueueList = new ArrayList<MessageQueue>();
// 每選擇一次訊息佇列,該值會自增1,如果Integer.MAX_VALUE,則重置為0,用於選擇訊息佇列。
private volatile ThreadLocalIndex sendWhichQueue = new ThreadLocalIndex();
private TopicRouteData topicRouteData;
}
public class TopicRouteData extends RemotingSerializable {
private String orderTopicConf;
// topic佇列後設資料
private List<QueueData> queueDatas;
// topic分佈的broker後設資料
private List<BrokerData> brokerDatas;
private HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;
}
具體資訊如下
{
"haveTopicRouterInfo": true,
"messageQueueList": [
{
"brokerName": "PQSZ-L0039",
"queueId": 0,
"topic": "TopicDemo"
},
{
"brokerName": "PQSZ-L0039",
"queueId": 1,
"topic": "TopicDemo"
},
{
"brokerName": "PQSZ-L0039",
"queueId": 2,
"topic": "TopicDemo"
},
{
"brokerName": "PQSZ-L0039",
"queueId": 3,
"topic": "TopicDemo"
}
],
"orderTopic": false,
"sendWhichQueue": {
"andIncrement": 1497938501
},
"topicRouteData": {
"brokerDatas": [
{
"brokerAddrs": {
"0": "10.178.42.122:10911"
},
"brokerName": "PQSZ-L0039",
"cluster": "DefaultCluster"
}
],
"filterServerTable": {},
"queueDatas": [
{
"brokerName": "PQSZ-L0039",
"perm": 6,
"readQueueNums": 4,
"topicSynFlag": 0,
"writeQueueNums": 4
}
]
}
}
選擇訊息佇列傳送訊息
根據主題路由資訊選擇訊息佇列
// DefaultMQProducerImpl.java
private SendResult sendDefaultImpl(
Message msg,
final CommunicationMode communicationMode,
final SendCallback sendCallback,
final long timeout
) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
// 省略......
if (topicPublishInfo != null && topicPublishInfo.ok()) {
boolean callTimeout = false;
MessageQueue mq = null;
Exception exception = null;
SendResult sendResult = null;
// 這裡會設定一個請求次數
// 如果為同步: timesTotal = 1 + 2,有兩次重試次數
// 如果為非同步: timesTotal = 1, 重試操作會在回撥介面中進行
int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;
int times = 0;
String[] brokersSent = new String[timesTotal];
for (; times < timesTotal; times++) {
String lastBrokerName = null == mq ? null : mq.getBrokerName();
// 選擇訊息佇列, 首次進入時 lastBrokerName = null
MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
if (mqSelected != null) {
mq = mqSelected;
brokersSent[times] = mq.getBrokerName();
// 省略......
// ......這裡會計算是否超時,如果超時會直接丟擲異常
// 傳送資訊
try{
sendResult = this.sendKernelImpl(
msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime);
// ......
this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false);
// ......
} catch (Exception e) {
// 這裡其實會有很多種異常,為了方便理解,直接在這裡寫成Exception
// 訊息傳送失敗
// 啟用Broker故障延遲機制,這裡會將這個BrokerName儲存起來
// 預設情況,使用30s來計算Broker故障規避時長,裡面會有一些計算邏輯
this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, true);
}
// 省略......
}
}
}
}
從上面程式碼可以發現,訊息傳送成功或者失敗都會呼叫MQFaultStrategy#updateFaultItem
,如果開啟了Broker故障延遲機制,裡面的程式碼才會執行。
/**
* @param brokerName broker名稱
* @param currentLatency 本次訊息傳送延遲時間 currentLatency
* @param isolation 是否隔離,該引數的含義如果為true,則使用30s來計算Broker故障規避時長,
* 如果為false,則使用本次訊息傳送延遲時間來計算Broker故障規避時長。
*
*/
public void updateFaultItem(final String brokerName, final long currentLatency, boolean isolation) {
if (this.sendLatencyFaultEnable) {
long duration = computeNotAvailableDuration(isolation ? 30000 : currentLatency);
this.latencyFaultTolerance.updateFaultItem(brokerName, currentLatency, duration);
}
}
選擇訊息佇列有兩種方式。
1)sendLatencyFaultEnable=false,預設不啟用Broker故障延遲機制。
2)sendLatencyFaultEnable=true,啟用Broker故障延遲機制。
不啟用Broker故障延遲機制時選擇佇列的方法入口在TopicPublishInfo#selectOneMessageQueue
public MessageQueue selectOneMessageQueue(final String lastBrokerName) {
// lastBrokerName == null 說明是第一次選擇訊息佇列
if (lastBrokerName == null) {
// 此時直接用sendWhichQueue自增再獲取值,與當前路由表中訊息佇列個數取模
return selectOneMessageQueue();
} else {
// lastBrokerName != null, 說明上一次傳送訊息失敗了,這一次是重試
int index = this.sendWhichQueue.getAndIncrement();
for (int i = 0; i < this.messageQueueList.size(); i++) {
int pos = Math.abs(index++) % this.messageQueueList.size();
if (pos < 0)
pos = 0;
MessageQueue mq = this.messageQueueList.get(pos);
// 上一次呼叫 lastBrokerName 失敗了,那麼這一次呼叫 lastBrokerName 也可能失敗
// 所以這裡會過濾掉上一次呼叫失敗的那個 Broker
if (!mq.getBrokerName().equals(lastBrokerName)) {
return mq;
}
}
// 如果上面沒有獲取佇列
// 此時直接用sendWhichQueue自增再獲取值,與當前路由表中訊息佇列個數取模
return selectOneMessageQueue();
}
}
public MessageQueue selectOneMessageQueue() {
// 用sendWhichQueue自增再獲取值
int index = this.sendWhichQueue.getAndIncrement();
// 拿 index 與佇列長度取模
int pos = Math.abs(index) % this.messageQueueList.size();
//
if (pos < 0)
pos = 0;
// 返回佇列
return this.messageQueueList.get(pos);
}
啟用Broker故障延遲機制,選擇訊息佇列入口在MQFaultStrategy#selectOneMessageQueue
public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {
if (this.sendLatencyFaultEnable) {
try {
int index = tpInfo.getSendWhichQueue().getAndIncrement();
for (int i = 0; i < tpInfo.getMessageQueueList().size(); i++) {
int pos = Math.abs(index++) % tpInfo.getMessageQueueList().size();
if (pos < 0)
pos = 0;
MessageQueue mq = tpInfo.getMessageQueueList().get(pos);
// 驗證該訊息佇列是否可用,裡面根據當前時間做對比
if (latencyFaultTolerance.isAvailable(mq.getBrokerName())) {
if (null == lastBrokerName || mq.getBrokerName().equals(lastBrokerName))
return mq;
}
}
// 嘗試從規避的Broker中選擇一個可用的Broker,如果沒有找到,將返回null
final String notBestBroker = latencyFaultTolerance.pickOneAtLeast();
int writeQueueNums = tpInfo.getQueueIdByBroker(notBestBroker);
if (writeQueueNums > 0) {
final MessageQueue mq = tpInfo.selectOneMessageQueue();
if (notBestBroker != null) {
mq.setBrokerName(notBestBroker);
mq.setQueueId(tpInfo.getSendWhichQueue().getAndIncrement() % writeQueueNums);
}
return mq;
} else {
latencyFaultTolerance.remove(notBestBroker);
}
} catch (Exception e) {
log.error("Error occurred when selecting message queue", e);
}
return tpInfo.selectOneMessageQueue();
}
return tpInfo.selectOneMessageQueue(lastBrokerName);
}
訊息佇列負載機制:訊息生產者在傳送訊息時,如果本地路由表中未快取topic的路由資訊,向NameServer傳送獲取路由資訊請求,更新本地路由資訊表,並且訊息生產者每隔30s從NameServer更新路由表。
訊息傳送異常機制:訊息傳送高可用主要透過兩個手段:重試與Broker規避。Broker規避就是在一次訊息傳送過程中發現錯誤,在某一時間段內,訊息生產者不會選擇該Broker(訊息伺服器)上的訊息佇列,提高傳送訊息的成功率。
本作品採用《CC 協議》,轉載必須註明作者和本文連結