《RocketMQ實戰與原理解析》學習筆記
https://help.aliyun.com/document_detail/29533.html
名詞解釋
topic && tag
Topic : 消息主題,一級消息類型
,通過 Topic 對消息進行分類。
Tag : 消息標籤,二級消息類型
,用來進一步區分某個 Topic 下的消息分類。 RMQ 允許消費者按照 Tag
對消息進行過濾,確保消費者最終只消費到他關注的消息類型。
//制定topic && tag 進行消息過濾
consumer.subscribe("topic1", "TagA || TagC || TagD");
到底什麼時候該用 Topic,什麼時候該用 Tag?
以天貓交易平臺爲例,訂單消息,支付消息屬於不同業務類型的消息,
- 分別創建 Topic_Order 和 Topic_Pay
兩個topic
- 其中訂單消息根據商品品類以
不同的 Tag
再進行細分,如電器類、男裝類、女裝類、化妝品類,最後他們都被各個不同的系統所接收。
通過合理的使用 Topic 和 Tag,可以讓業務結構清晰,更可以提高效率。
Message
Message : 消息,消息隊列中信息傳遞的載體。
Message ID: 消息的全局唯一標識,由消息隊列 RMQ 系統自動生成
,唯一標識某條消息。
Message Key: 消息的業務標識,由消息生產者(Producer)設置,唯一標識某個業務邏輯
。
普通消息 (集羣消息)
一個 Group ID 所標識的所有 Consumer 平均分攤消費消息。
例如某個 Topic 有 9 條消息,一個 Group ID 有 3 個 Consumer 實例,那麼在集羣消費模式下每個實例平均分攤,只消費其中的 3 條消息
。
廣播消息
一個 Group ID 所標識的所有 Consumer 都會各自消費某條消息一次。例如某個 Topic 有 9 條消息,一個 Group ID 有 3 個 Consumer 實例,那麼在廣播消費模式下每個實例都會各自消費 9 條消息
.
定時消息 && 延時消息:
定時消息
Producer 將消息發送到消息隊列 RocketMQ 服務端,但並不期望這條消息立馬投遞,而是推遲到在當前時間點之後的某一個時間投遞到 Consumer 進行消費,該消息即定時消息。
延時消息
Producer 將消息發送到消息隊列 RocketMQ 服務端,但並不期望這條消息立馬投遞,而是延遲一定時間後才投遞到 Consumer 進行消費,該消息即延時消息。
順序消息
消息隊列 RocketMQ 提供的一種按照順序進行發佈和消費的消息類型,分爲全局順序消息
和分區順序消息
。
全局順序消息
對於指定的一個 Topic,所有消息按照嚴格的先入先出(FIFO)的順序進行發佈和消費。
分區順序消息
對於指定的一個 Topic,所有消息根據 Sharding Key 進行區塊分區。同一個分區內的消息按照嚴格的 FIFO 順序進行發佈和消費。Sharding Key
是順序消息中用來區分不同分區的關鍵字段,和普通消息的 Message Key
是完全不同的概念。
事務消息
消息隊列 RocketMQ 提供類似 X/Open XA 的分佈事務功能,通過消息隊列 RocketMQ 的事務消息能達到分佈式事務的最終一致.
不同類型消息的發送&消費
普通消息
普通消息是指消息隊列 RMQ 中無特性的消息,區別於有特性的在這裏插入代碼片
定時/延時消息、順序消息和事務消息。
發送普通消息(3種方式)
可靠同步發送
: 同步發送是指消息發送方發出數據後,會在收到接收方發回響應之後才發下一個數據包的通訊方式。
可靠異步發送
: 異步發送是指發送方發出數據後,不等接收方發回響應,接着發送下個數據包的通訊方式。 消息隊列 RocketMQ 的異步發送,需要用戶實現異步發送回調接口(SendCallback)
。消息發送方在發送了一條消息後,不需要等待服務器響應即可返回,進行第二條消息發送。發送方通過回調接口接收服務器響應
,並對響應結果進行處理。
單向(Oneway)發送
: 單向(Oneway)發送特點爲發送方只負責發送消息,不等待服務器迴應且沒有回調函數觸發,即只發送請求不等待應答
。
下表概括了三者的特點和主要區別。
demo
public class DifferentWaySend {
private static int TASK_NUM = 100;
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("gp1");
producer.setNamesrvAddr("s157:9876;s158:9876");
producer.start();
List<Message> messageList = new ArrayList<>();
IntStream.rangeClosed(1,TASK_NUM).forEach(taskNo ->{
try {
messageList.add( new Message(
"topic1",
"tag1_1",
("Hello RocketMQ " + taskNo).getBytes(RemotingHelper.DEFAULT_CHARSET)));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
});
sync(producer,messageList);
async(producer,messageList);
oneway(producer,messageList);
producer.shutdown();
}
public static void sync(DefaultMQProducer producer , List<Message> messageList) throws Exception {
Instant start= Instant.now();
for(Message msg : messageList) {
//同步發送消息
SendResult sendResult = producer.send(msg);
// System.out.println("sync:"+sendResult.getMsgId());
}
Instant end= Instant.now();
System.out.println("-----------------------------------sync 共耗時:" + ChronoUnit.MILLIS.between(start,end) + "ms");
}
public static void async(DefaultMQProducer producer , List<Message> messageList) throws Exception {
CountDownLatch countDownLatch = new CountDownLatch(TASK_NUM);
Instant start= Instant.now();
for(Message msg : messageList) {
producer.send(msg,new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
// System.out.println("async:"+sendResult.getMsgId());
countDownLatch.countDown();
}
@Override
public void onException(Throwable e) {
System.out.println("async:"+e.getMessage());
countDownLatch.countDown();
}
});
}
countDownLatch.await();
Instant end= Instant.now();
System.out.println("-----------------------------------async 共耗時:" + ChronoUnit.MILLIS.between(start,end) + "ms");
}
public static void oneway(DefaultMQProducer producer , List<Message> messageList) throws Exception {
Instant start= Instant.now();
for(Message msg : messageList) {
// 由於在 oneway 方式發送消息時沒有請求應答處理,一旦出現消息發送失敗,則會因爲沒有重試而導致數據丟失。若數據不可丟,建議選用可靠同步或可靠異步發送方式。
producer.sendOneway(msg);
}
Instant end= Instant.now();
System.out.println("-----------------------------------oneway 共耗時:" + ChronoUnit.MILLIS.between(start,end) + "ms");
}
}
執行結果
:
消費消息(集羣模式&&廣播模式)
//設置爲廣播模式
consumer.setMessageModel(MessageModel.BROADCASTING);
//集羣模式(默認)
consumer.setMessageModel(MessageModel.CLUSTERING);
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> messages, ConsumeConcurrentlyContext context) {
for (MessageExt message : messages) {
System.out.println(message);
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
定時&延時消息
定時消息與延時消息在代碼配置上存在一些差異,但是最終達到的效果相同:消息在發送到消息隊列 RocketMQ 服務端後並不會立馬投遞,而是根據消息中的屬性延遲固定時間後才投遞給消費者。
- 發送定時消息需要明確指定消息發送時間點之後的某一時間點作爲消息投遞的時間點。
- 發送延時消息時需要設定一個延時時間長度,消息將從當前發送時間點開始延遲固定時間之後纔開始投遞。
setStartDeliverTime()
// 延時消息,單位毫秒(ms),在指定延遲時間(當前時間之後)進行投遞,例如消息在 3 秒後投遞
long delayTime = System.currentTimeMillis() + 3000;
// 設置消息需要被投遞的時間
msg.setStartDeliverTime(delayTime);
// 定時消息,單位毫秒(ms),在指定時間戳(當前時間之後)進行投遞,例如 2019-08-01 16:21:00 投遞。如果被設置成當前時間戳之前的某個時刻,消息將立刻投遞給消費者。
long timeStamp = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2019-08-01 16:21:00").getTime();
msg.setStartDeliverTime(timeStamp);
在
開源版本中
,RMQ並不支持精度爲秒級別的延遲消息
。
setDelayTimeLevel()
開源版本中,只支持特定的延時級別level
。
在服務器端(rocketmq-broker端)的屬性配置文件中加入以下行:
messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
來設置默認級別(上述爲默認配置).
//level=0 級表示不延時,level=1 表示 1 級延時,level=2 表示 2 級延時,以此類推。
//level==3 ,表示10s後投遞任務
message.setDelayTimeLevel(3);
收發定時&&延時消息
與普通消息的收發方式相同, 不同的是消息的屬性
不同。
順序消息
順序消息指消息發佈和消息消費都按順序進行。
- 順序發佈:對於指定的一個 Topic,客戶端將按照一定的先後順序發送消息。
- 順序消費:對於指定的一個 Topic,按照一定的先後順序接收消息,即先發送的消息一定會先被客戶端接收到。
全局順序
對於指定的一個 Topic,所有消息按照嚴格的先入先出(FIFO)的順序進行發佈和消費。
示例
在證券處理中,以人民幣兌換美元爲 Topic,在價格相同的情況下,先出價者優先處理
,則可以通過全局順序的方式按照 FIFO 的方式進行發佈和消費。
分區順序
對於指定的一個 Topic,所有消息根據 sharding key 進行區塊分區。 同一個分區內的消息按照嚴格的 FIFO 順序進行發佈和消費。
Sharding key 是順序消息中用來區分不同分區的關鍵字段,和普通消息的 Key 是完全不同的概念。
示例
例一
:用戶註冊需要發送發驗證碼,以用戶 ID
作爲 sharding key, 那麼同一個用戶發送的消息都會按照先後順序來發布和消費。
例二
:電商的訂單創建,以訂單 ID
作爲 sharding key,那麼同一個訂單相關的創建訂單消息、訂單支付消息、訂單退款消息、訂單物流消息都會按照先後順序來發布和消費。
消息類型對比
發送方式對比
發送順序消息-MessageQueueSelector()
String orderId = "Order_0000001";
//msg-key: "PAY_201907151223001" 標識此條消息業務id
//msg-key: 以方便您在無法正常收到消息情況下,可通過控制檯查詢消息並補發。
String payId = "PAY_201907151223001";
Message msg = new Message("pay", "TAG1", "PAY_201907151223001" ,
("支付消息,內容爲:xxxxx " ).getBytes(RemotingHelper.DEFAULT_CHARSET));
// 分區順序消息中區分不同分區的關鍵字段,sharding key 於普通消息的 key 是完全不同的概念。
// 全局順序消息,該字段可以設置爲任意非空一個字符串常量即可。
String shardingKey = orderId;
SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
//arg爲後續傳遞的shardingKey,可以根據hash算法or其他方法來計算出id;
//可參考hashmap的hash算法;
int id = hash(arg);
int index = id % mqs.size();
return mqs.get(index);
}
}, shardingKey);
接收順序消息-MessageListenerOrderly()
consumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeOrderlyContext context) {
//處理消息...
return ConsumeOrderlyStatus.SUCCESS;
}
});
多知道一點—分佈式事務
引用: https://www.jianshu.com/p/c26b3af5880f
微服務倡導將複雜的系統拆分爲若干個簡單、職責單一、松耦合的服務,可以降低開發難度,便於敏捷開發。而對大多數中小型公司來說,實施微服務架構面臨以下困難:
- 單體應用拆分爲分佈式系統後,應用間的通訊和故障處理機制變得複雜
- 微服務化後,一個簡單的功能需要調用多個服務並操作多個數據庫實現,數據一致性難以保障
- 大量的微服務,導致其測試、維護、部署變得困難
爲了保障微服務架構下數據的一致性,通常需要引入分佈式事務來解決,當前比較流行的分佈式解決方案如下。
基於二階段提交的XA協議
- 第一階段:協調者詢問所有參與者是否可以執行提交操作,參與者執行準備工作,例如爲資源上鎖,預留資源,寫undo/redo log。
- 第二階段:若所有參與者迴應“可提交”,則向所有參與者發送正式提交命令;若某個參與者迴應“拒絕提交”,則向所有參與者發送回滾命令。
XA協議保障了事務的強一致性,然而由於其採用的阻塞協議
帶來的巨大性能開銷,難以達到較高的系統吞吐量。
TCC模式
TCC提供了一種全局事務解決方案,業務系統只需實現下面三個操作,即可完成分佈式事務:
- TRY:完成參與者業務檢查並預留業務資源
- CONFIRM:使用TRY階段的預留業務資源,並執行業務
- CANCEL:釋放TRY結算預留的業務資源
TCC模式可以讓業務更靈活地定義數據庫操作的粒度,使得降低鎖衝突、提高吞吐量成爲可能,然而它對業務的侵入度較高,實現難度較大
。
事務消息
通過消息的異步事務,可以保證本地事務和消息發送同時執行成功或失敗,從而保證了數據的最終一致性。
- 發送prepare消息,該消息對
Consumer不可見
- 執行本地事務
- 若本地事務執行成功,則向MQ提交消息確認發送指令;若本地事務執行失敗,則向MQ發送取消指令
- 若MQ長時間未收到確認發送或取消發送的指令,則向業務系統詢問本地事務狀態,並做補償處理
RMQ事務消息
其中:
- 發送方向消息隊列 RocketMQ 服務端發送消息。
- 服務端將消息持久化成功之後,向發送方 ACK 確認消息已經發送成功,此時消息爲半消息。
- 發送方開始執行本地事務邏輯。
- 發送方根據本地事務執行結果向服務端提交二次確認(Commit 或是 Rollback),服務端收到 Commit 狀態則將半消息標記爲可投遞,訂閱方最終將收到該消息;服務端收到 Rollback 狀態則刪除半消息,訂閱方將不會接受該消息。
- 在斷網或者是應用重啓的特殊情況下,上述步驟 4 提交的二次確認最終未到達服務端,經過固定時間後服務端將對該消息發起消息回查。
- 發送方收到消息回查後,需要檢查對應消息的本地事務執行的最終結果。
- 發送方根據檢查得到的本地事務的最終狀態再次提交二次確認,服務端仍按照步驟 4 對半消息進行操作。
說明:事務消息發送對應步驟 1、2、3、4,事務消息回查對應步驟 5、6、7
。
關鍵代碼
- producer
TransactionListener transactionListener = new DeducationTransactionListenerImpl();
//`producer`需要綁定transactionListener
producer.setTransactionListener(transactionListener);
//`producer`需要sendMessageInTransaction方法發送消息
SendResult sendResult = producer.sendMessageInTransaction(msg, null);
- TransactionListener
public class DeducationTransactionListenerImpl implements TransactionListener {
//當發送prepare(half)消息成功後,會執行此邏輯
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
LocalTransactionState state ;
//todo 執行業務方法,並根據執行結果,返回state
return state;
}
/**
* 當沒有迴應prepare(half)消息時,brokder會檢查此條消息的狀態
* @param msg
* @return
*/
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
LocalTransactionState state ;
//todo 查看訂單bizNo的狀態,並返回state
return state;
}
}
TransactionStatus
TransactionStatus.CommitTransaction
提交事務,允許訂閱方消費該消息。TransactionStatus.RollbackTransaction
回滾事務,消息將被丟棄不允許消費。TransactionStatus.Unknow
暫時無法判斷狀態,期待固定時間以後消息隊列 RocketMQ 服務端向發送方進行消息回查。
Message設置消息回查時間
/**
* 在消息屬性中添加第一次消息回查的最快時間,單位秒。
* 例如,以下設置實際第一次回查時間爲 120 秒 ~ 125 秒之間
*
* 以上方式只確定事務消息的第一次回查的最快時間,實際回查時間向後浮動0~5秒;
* 如第一次回查後事務仍未提交,後續每隔5秒回查一次。
*/
msg.putUserProperty(MessageConst.PROPERTY_CHECK_IMMUNITY_TIME_IN_SECONDS,"120");