微服務異步架構——MQ之RocketMQ

本文轉載自:微服務異步架構——MQ之RocketMQ


“我們大家都知道把一個微服務架構變成一個異步架構只需要加一個MQ,現在市面上有很多MQ的開源框架。到底選擇哪一個MQ的開源框架才合適呢 ”

一、什麼是MQ?MQ的原理是什麼?

MQ就是消息隊列,是Message Queue的縮寫。消息隊列是一種通信方式。消息的本質就是一種數據結構。因爲MQ把項目中的消息集中式的處理和存儲,所以MQ主要有解耦,併發,和削峯的功能。

1.1 解耦

MQ的消息生產者和消費者互相不關心對方是否存在,通過MQ這個中間件的存在,使整個系統達到解耦的作用。

如果服務之間用RPC通信,當一個服務跟幾百個服務通信時,如果那個服務的通信接口改變,那麼幾百個服務的通信接口都的跟着變動,這是非常頭疼的一件事。

但是採用MQ之後,不管是生產者或者消費者都可以單獨改變自己。他們的改變不會影響到別的服務。從而達到解耦的目的。爲什麼要解耦呢?說白了就是方便,減少不必要的工作量。

1.2 併發

MQ有生產者集羣和消費者集羣,所以客戶端是億級用戶時,他們都是並行的。從而大大提升響應速度。

1.3 削峯

因爲MQ能存儲的消息量很大,所以他可以把大量的消息請求先存下了,然後再併發的方式慢慢處理。

如果採用RPC通信,每一次請求用調用RPC接口,當請求量巨大的時候,因爲RPC的請求是很耗資源的,所以巨大的請求一定會壓垮服務器。

削峯的目的是用戶體驗變好,並且使整個系統穩定。能承受大量請求消息。

二、現在市面上有什麼MQ

重點介紹RocketMQ

現在市面上的MQ有很多,主要有RabbitMQ,ActiveMQ,ZeroMQ,RocketMQ,Kafka等等,這些都是開源的MQ產品。以前很多人推薦使用RabbitMQ,他也是非常好用的MQ產品,這裏不做過多的介紹。Kafka也是高吞吐量的老大,我們這裏也不介紹。

我們重點介紹一下RocketMQ,RocketMQ是阿里巴巴在2012年開源的分佈式消息中間件,目前已經捐贈給Apache軟件基金會,並於並於2017年9月25日成爲 Apache 的頂級項目。

作爲經歷過多次阿里巴巴雙十一這種“超級工程”的洗禮並有穩定出色表現的國產中間件,以其高性能、低延時和高可靠等特性近年來已經也被越來越多的國內企業使用。

功能概覽圖

可以看見RocketMQ支持定時和延時消息,這是RabbitMQ所沒有的能力。

RocketMQ的物理結構

從這裏可以看出,RocketMQ涉及到四大集羣,producer,Name Server,Consumer,Broker。

2.1 Producer集羣:

是生產者集羣,負責產生消息,向消費者發送由業務應用程序系統生成的消息,RocketMQ提供三種方式發送消息:同步,異步,單向。

2.1.1 普通消息

2.1.1.1 同步原理圖

同步消息關鍵代碼

try {
	SendResult sendResult =
	producer.send(msg);
	// 同步發送消息,
	只要不拋異常就是成功 if
	(sendResult != null) {
		System.out.println
		(new Date() + " Send mq message success.
Topic is:" + msg.getTopic() + " msgId is: " + sendResult.getMessageId());
	}
	catch (Exception e) {
		System.out.println
		(new Date() + " Send mq message failed.
Topic is:" + msg.getTopic());
		e.printStackTrace();
	}
}

2.1.1.2 異步原理圖

異步消息關鍵代碼

producer.sendAsync(msg, new SendCallback()
{
	@Overridepublic void onSuccess
	(final SendResult sendResult)
	{
		// 消費發送成功 System.out.println
		("send message success. topic=" +
		sendResult.getTopic() + ", msgId="
		+ sendResult.getMessageId());
	}
	@Overridepublic void onException
	(OnExceptionContext context)
	{
		System.out.println("send message failed.
topic=" + context.getTopic() + ",
msgId=" + context.getMessageId());
	}
}
);

2.1.1.3 單向(Oneway)發送原理圖

單向只發送,不等待返回,所以速度最快,一般在微秒級,但可能丟失

單向(Oneway)發送消息關鍵代碼

producer.sendOneway(msg);

2.1.2 定時消息和延時消息

發送定時消息關鍵代碼

try {
	// 定時消息,單位毫秒(ms),
	在指定時間戳(當前時間之後)進行投遞,
	例如 2016-03-07 16:21:00 投遞。
	如果被設置成當前時間戳之前的某個時刻,
	消息將立刻投遞給消費者。 long timeStamp
	= new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
	.parse("2016-03-07 16:21:00").getTime();
	msg.setStartDeliverTime(timeStamp);
	// 發送消息,只要不拋異常就是成功
	SendResult sendResult = producer.send(msg);
	System.out.println
	("MessageId:"+sendResult.getMessageId());
}
catch (Exception e) {
	// 消息發送失敗,
	需要進行重試處理,可重新發送這條消息
	或持久化這條數據進行補償處理
	System.out.println(new Date()
	+ " Send mq message failed.
Topic is:" + msg.getTopic());
	e.printStackTrace();
}

發送延時消息關鍵代碼

try {
	// 延時消息,單位毫秒(ms),
	在指定延遲時間(當前時間之後)進行投遞,
	例如消息在 3 秒後投遞 long delayTime
	= System.currentTimeMillis() + 3000;
	// 設置消息需要被投遞的時間
	msg.setStartDeliverTime(delayTime);
	SendResult sendResult = producer.send(msg);
	// 同步發送消息,只要不拋異常就是成功
	if (sendResult != null)
	{
		System.out.println(new Date()
		+ " Send mq message success.
Topic is:" + msg.getTopic() + " msgId is: " + sendResult.getMessageId());
	}
}
catch (Exception e) {
	// 消息發送失敗,
	需要進行重試處理,可重新發送這條消息
	或持久化這條數據進行補償處理
	System.out.println(new Date()
	+ " Send mq message failed.
Topic is:" + msg.getTopic());
	e.printStackTrace();
}

2.2 注意事項

①. 定時和延時消息的 msg.setStartDeliverTime 參數需要設置成當前時間戳之後的某個時刻(單位毫秒)。如果被設置成當前時間戳之前的某個時刻,消息將立刻投遞給消費者。

②. 定時和延時消息的 msg.setStartDeliverTime 參數可設置40天內的任何時刻(單位毫秒),超過40天消息發送將失敗。

③. StartDeliverTime 是服務端開始向消費端投遞的時間。 如果消費者當前有消息堆積,那麼定時和延時消息會排在堆積消息後面,將不能嚴格按照配置的時間進行投遞。

④. 由於客戶端和服務端可能存在時間差,消息的實際投遞時間與客戶端設置的投遞時間之間可能存在偏差。

⑤. 設置定時和延時消息的投遞時間後,依然受 3 天的消息保存時長限制。例如,設置定時消息 5 天后才能被消費,如果第 5 天后一直沒被消費,那麼這條消息將在第8天被刪除。

⑥. 除 Java 語言支持延時消息外,其他語言都不支持延時消息。

發佈消息原理圖

三、事務消息

RocketMQ提供類似X/Open XA的分佈式事務功能來確保業務發送方和MQ消息的最終一致性,其本質是通過半消息的方式把分佈式事務放在MQ端來處理。

原理圖

其中:

①. 發送方向消息隊列 RocketMQ 服務端發送消息。

②. 服務端將消息持久化成功之後,向發送方 ACK 確認消息已經發送成功,此時消息爲半消息。

③. 發送方開始執行本地事務邏輯。

④. 發送方根據本地事務執行結果向服務端提交二次確認(Commit 或是 Rollback),服務端收到 Commit 狀態則將半消息標記爲可投遞,訂閱方最終將收到該消息;服務端收到 Rollback 狀態則刪除半消息,訂閱方將不會接受該消息。

⑤. 在斷網或者是應用重啓的特殊情況下,上述步驟 4 提交的二次確認最終未到達服務端,經過固定時間後服務端將對該消息發起消息回查。

⑥. 發送方收到消息回查後,需要檢查對應消息的本地事務執行的最終結果。

⑦. 發送方根據檢查得到的本地事務的最終狀態再次提交二次確認,服務端仍按照步驟 4 對半消息進行操作。

3.1 RocketMQ的半消息機制的注意事項是

①. 根據第六步可以看出他要求發送方提供業務回查接口。

②. 不能保證發送方的消息冪等,在ack沒有返回的情況下,可能存在重複消息

③. 消費方要做冪等處理。

3.2 核心代碼

final BusinessService businessService = new BusinessService(); // 本地業務

TransactionProducer producer
= ONSFactory.createTransactionProducer
(properties,new LocalTransactionCheckerImpl());
producer.start();
Message msg = new Message
("Topic", "TagA", "Hello MQ transaction===".
getBytes());
try {
	SendResult sendResult
	= producer.send(msg, new LocalTransactionExecuter()
	{
		@Override public TransactionStatus execute
		(Message msg, Object arg) {
			// 消息 ID
			(有可能消息體一樣,但消息 ID 不一樣,
			當前消息 ID 在控制檯無法查詢)
			String msgId = msg.getMsgID();
			// 消息體內容進行 crc32,也可以使用其它的如
			MD5 long crc32Id = HashUtil.crc32Code
			(msg.getBody());
			// 消息 ID 和 crc32id
			主要是用來防止消息重複 // 如果業務本身是冪等的,
			可以忽略,否則需要利用 msgId 或 crc32Id 來做冪等
			// 如果要求消息絕對不重複,推薦做法是
			對消息體 body 使用 crc32 或 MD5 來防止重複消息
			Object businessServiceArgs = new Object();
			TransactionStatus transactionStatus
			=TransactionStatus.Unknow;
			try {
				Boolean isCommit =
				businessService.execbusinessService
				(businessServiceArgs);
				if (isCommit)
				{
					// 本地事務成功則提交消息 transactionStatus = TransactionStatus.CommitTransaction; } else {
						// 本地事務失敗則回滾消息
						transactionStatus = TransactionStatus.
						RollbackTransaction;
					}
				}
				catch (Exception e)
				{
					log.error("Message Id:{}", msgId, e);
				}
				System.out.println(msg.getMsgID());
				log.warn("Message Id:{}transactionStatus:{}", msgId, transactionStatus.name());
				return transactionStatus;
			}
		}
		, null);
	}
	catch (Exception e) {
		// 消息發送失敗,
		需要進行重試處理,可重新發送這條消息或
		持久化這條數據進行補償處理 System.out.println
		(new Date() + " Send mq message failed.
Topic is:" + msg.getTopic());
		e.printStackTrace();
	}

所有消息發佈原理圖

producer完全無狀態,可以集羣部署。

3.3 Name Server集羣

NameServer是一個幾乎無狀態的節點,可集羣部署,節點之間無任何信息同步,NameServer很像註冊中心的功能。

聽說阿里之前的NameServer 是用ZooKeeper做的,可能因爲Zookeeper不能滿足大規模併發的要求,所以之後NameServer 是阿里自研的。

NameServer其實就是一個路由表,他管理Producer和Comsumer之間的發現和註冊。

3.4 Broker集羣

Broker部署相對複雜,Broker分爲Master與Slave,一個Master可以對應多個Slaver,但是一個Slaver只能對應一個Master,Master與Slaver的對應關係通過指定相同的BrokerName。

不同的BrokerId來定義,BrokerId爲0表示Master,非0表示Slaver。Master可以部署多個。每個Broker與NameServer集羣中的所有節點建立長連接,定時註冊Topic信息到所有的NameServer。

3.5 Consumer集羣

訂閱方式

消息隊列 RocketMQ 支持以下兩種訂閱方式:

集羣訂閱:同一個 Group ID 所標識的所有 Consumer 平均分攤消費消息。 例如某個 Topic 有 9 條消息,一個 Group ID 有 3 個 Consumer 實例,那麼在集羣消費模式下每個實例平均分攤,只消費其中的 3 條消息。

// 集羣訂閱方式設置(不設置的情況下,
默認爲集羣訂閱方式)properties.put
(PropertyKeyConst.MessageModel,
PropertyValueConst.CLUSTERING);

廣播訂閱:同一個 Group ID 所標識的所有 Consumer 都會各自消費某條消息一次。 例如某個 Topic 有 9 條消息,一個 Group ID 有 3 個 Consumer 實例,那麼在廣播消費模式下每個實例都會各自消費 9 條消息。

// 廣播訂閱方式設置properties.put
(PropertyKeyConst.MessageModel,
PropertyValueConst.BR
OADCASTING);

訂閱消息關鍵代碼:

Consumer consumer = ONSFactory.create
Consumer(properties);
consumer.subscribe
("TopicTestMQ", "TagA||TagB", **new**
MessageListener() {
	//訂閱多個
	Tag public Action consume(Message message,
	ConsumeContext context) {
		System.out.println
		("Receive: " + message);
		return Action.
		CommitMessage;
	}
}
);
//訂閱另外一個 Topic
consumer.subscribe("TopicTestMQ-Other",
"*", **new** MessageListener()
{
	//訂閱全部 Tag public Action consume
	(Message message, ConsumeContext context)
	{
		System.out.println("Receive: " + message);
		return Action.CommitMessage;
	}
}
);
consumer.start();

注意事項:

消費端要做冪等處理,所有MQ基本上都不會做冪等處理,需要業務端處理,原因是如果在MQ端做冪等處理會帶來MQ的複雜度,而且嚴重影響MQ的性能。

消息收發模型

主子賬號創建

創建主子賬號的原因是權限問題。下面是主賬號創建流程圖

子賬號流程圖

四、MQ是微服務架構

非常重要的部分

MQ的誕生把原來的同步架構思維轉變到異步架構思維提供一種方法,爲大規模,高併發的業務場景的穩定性實現提供了很好的解決思路。

Martin Fowler強調:分佈式調用的第一原則就是不要分佈式。這句話看似頗具哲理,然而就企業應用系統而言,只要整個系統在不停地演化,並有多個子系統共同存在時,這條原則就會被迫打破。

Martin Fowler提出的這條原則,一方面是希望設計者能夠審慎地對待分佈式調用,另一方面卻也是分佈式系統自身存在的缺陷所致。

所以微服務並不是萬能藥,適合的架構纔是最好的架構。


本文轉載自:微服務異步架構——MQ之RocketMQ

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