【RocketMQ】高階使用:四個問題詳解事務訊息
RocketMQ和其他訊息中介軟體最大的一個區別是支援了事務訊息,這也是分散式事務裡面的基於訊息的最終一致性方案。
1.事務訊息是什麼?
事務訊息:具有事務特性的訊息,即Producer傳送到broker後,該訊息可以回滾或者提交(提交後Consumer才可見)。
2.事務訊息有什麼用?
RocketMQ官方示例:使用者A發起訂單,支付100塊錢操作完成後,能得到100積分,賬戶服務和會員服務是兩個獨立的微服務模組,有各自的資料庫,按照上文提及的問題可能性,將會出現這些情況:
-
如果先扣款,再發訊息,可能錢剛扣完,當機了,訊息沒發出去,結果積分沒增加。
-
如果先發訊息,再扣款,可能積分增加了,但錢沒扣掉,白送了100積分給人家。
// 先扣款,再加積分虛擬碼:
@Transational
pay() {
mysql.payMoney() // 資料庫中新增使用者資訊
reduceRepo() // 資料庫中減少庫存
}
-------------------------------------// 若先發訊息再加積分,那在這行當機怎麼辦?
producer.send(msg) // 傳送100積分
==> 所以上述方式不可行,我們可以先傳送訊息(加積分)到Broker,但將訊息置為Consumer不可見狀態
- 若本地事務(扣款)處理成功了再讓Consumer可見
- 若本地事務(扣款)失敗了就回滾當前訊息
這裡可能會存在一個問題,生產者本地事務成功後,傳送事務確認訊息到broker上失敗了怎麼辦?
這個時候意味著消費者無法正常消費到這個訊息。所以RocketMQ提供了訊息回查機制,如果事務訊息一直處於中間狀態,broker會發起重試去查詢broker上這個事務的處理狀態。一旦發現事務處理成功,則把當前這條訊息設定為可見
整體的模型圖如下:
從上例我們看見,事務訊息一般先於本地事務使用。這裡也可以理解成巢狀事務,發訊息是外層事務,本地事務是記憶體事務。
3.java使用事務訊息?
針對上面的示例,我們來看看如何通過具體的程式碼實現。
TransactionProducer
public class TransactionProducer {
public static void main(String[] args) throws Exception {
// 這裡用的是事務Producer(TransactionMQProducer)
TransactionMQProducer transactionProducer=new TransactionMQProducer("tx_producer_group");
transactionProducer.setNamesrvAddr("43.105.136.120:9876");
// 自定義執行緒池,用於非同步執行事務操作
transactionProducer.setExecutorService(Executors.newFixedThreadPool(10); );
// 核心!!新增事務訊息監聽
transactionProducer.setTransactionListener(new TransactionListenerLocal());
transactionProducer.start();
for(int i=0;i<20;i++) {
String orderId= UUID.randomUUID().toString();
String body="{'operation':'doOrder','orderId':'"+orderId+"'}";
// 構建訊息
Message message = new Message("pay_tx_topic", "TagA",orderId, body.getBytes(RemotingHelper.DEFAULT_CHARSET));
// 傳送訊息, 注:是傳送事務訊息
transactionProducer.sendMessageInTransaction(message, orderId+"&"+i);
Thread.sleep(1000); // 1秒一次
}
}
}
TransactionListenerLocal(事務訊息核心)
// 本地事務監聽,實現TransactionListener介面
public class TransactionListenerLocal implements TransactionListener {
private static final Map<String,Boolean> results=new ConcurrentHashMap<>();
// 執行本地事務
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
System.out.println(":執行本地事務:"+arg.toString());
String orderId=arg.toString();
// 模擬資料入庫操作(成功/失敗)
boolean rs=saveOrder(orderId);
return rs? LocalTransactionState.COMMIT_MESSAGE:LocalTransactionState.UNKNOW;
// 這個返回狀態表示告訴broker這個事務訊息是否被確認,允許給到consumer進行消費
// LocalTransactionState.ROLLBACK_MESSAGE 回滾
// LocalTransactionState.UNKNOW 未知
}
// 提供事務執行狀態的回查方法,提供給broker回撥
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
String orderId=msg.getKeys();
System.out.println("執行事務執行狀態的回查,orderId:"+orderId);
boolean rs=Boolean.TRUE.equals(results.get(orderId));
System.out.println("回撥:"+rs);
return rs?LocalTransactionState.COMMIT_MESSAGE:
LocalTransactionState.ROLLBACK_MESSAGE;
}
private boolean saveOrder(String orderId){
//如果訂單取模等於0,表示成功,否則表示失敗
boolean success=Math.abs(Objects.hash(orderId))%2==0;
results.put(orderId,success);
return success;
}
}
TransactionConsumer
public class TransactionConsumer {
public static void main(String[] args) throws MQClientException, IOException {
DefaultMQPushConsumer defaultMQPushConsumer=new
DefaultMQPushConsumer("tx_consumer_group");
defaultMQPushConsumer.setNamesrvAddr("43.105.136.120:9876");
defaultMQPushConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_O FFSET);
defaultMQPushConsumer.subscribe("pay_tx_topic","*");
defaultMQPushConsumer.registerMessageListener((MessageListenerConcurrently)
(msgs, context) -> {
msgs.stream().forEach(messageExt -> {
try {
String orderId=messageExt.getKeys();
// 拿到訊息
String body=new String(messageExt.getBody(),
RemotingHelper.DEFAULT_CHARSET);
// 扣減庫存
System.out.println("收到訊息:"+body+",開始扣減庫存:"+orderId);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
});
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
defaultMQPushConsumer.start();
System.in.read();
}
}
4.事務訊息的三種狀態?
在上面的 TransactionListenerLocal 類中,我們看見重寫的兩個方法都需要返回 LocalTransactionState,表示告訴broker對於這條已經存在了的事務訊息如何處理:
-
ROLLBACK_MESSAGE:回滾事務
當executeLocalTransaction方法返回ROLLBACK_MESSAGE時,表示直接回滾事務
-
COMMIT_MESSAGE: 提交事務
-
UNKNOW: broker會定時的回查Producer訊息狀態,直到徹底成功或失敗。
當返回UNKNOW時,Broker會在一段時間之後回查checkLocalTransaction,根據 checkLocalTransaction返回狀態執行事務的操作(回滾或提交)
如示例中,當返回 ROLLBACK_MESSAGE 時消費者不會收到訊息,且不會呼叫回查函式,當返回 COMMIT_MESSAGE 時事務提交,消費者收到訊息,當返回UNKNOW時,在一段時間之後呼叫回查函式,並根據status判斷返回提交或回滾狀態,返回提交狀態的訊息將會被消費者消費,所以此時消費者可以消費部分訊息。
相關文章
- rocketmq有序訊息的(四)MQ
- 解析 RocketMQ 業務訊息——“事務訊息”MQ
- RocketMq訊息丟失問題解決MQ
- 解析 RocketMQ 業務訊息--“順序訊息”MQ
- RocketMQ 訊息整合:多型別業務訊息-普通訊息MQ多型型別
- RocketMQ 分散式事務訊息MQ分散式
- RocketMQ 訊息整合:多型別業務訊息——定時訊息MQ多型型別
- RocketMQ訊息丟失解決方案:事務訊息MQ
- RocketMQ 原理:訊息儲存、高可用、訊息重試、訊息冪等性MQ
- Python使用RocketMQ(訊息佇列)PythonMQ佇列
- 深入理解 RocketMQ -事務訊息MQ
- RocketMQ與MYSQL事務訊息整合MQMySql
- 一張圖進階 RocketMQ - 訊息傳送MQ
- rocketmq事務訊息入門介紹MQ
- RocketMQ普通訊息MQ
- RocketMQ高階特性MQ
- RabbitMQ學習(三)之 “訊息佇列高階使用”MQ佇列
- 關於 RocketMQ ClientID 相同引發的訊息堆積的問題MQclient
- [訊息佇列]rocketMQ佇列MQ
- RocketMQ -- 訊息拉取MQ
- RocketMQ訊息權重MQ
- 訊息佇列之事務訊息,RocketMQ 和 Kafka 是如何做的?佇列MQKafka
- 訊息中介軟體—RocketMQ訊息傳送MQ
- 分散式事務利器——RocketMQ事務訊息的啟示分散式MQ
- RocketMQ的事務訊息處理【half-message】MQ
- 使用 Kotlin+RocketMQ 實現延時訊息KotlinMQ
- 【RocketMq-Producer】訊息傳送者引數詳解MQ
- 搞懂分散式技術19:使用RocketMQ事務訊息解決分散式事務分散式MQ
- RocketMQ架構原理解析(四):訊息生產端(Producer)MQ架構
- 聊聊 RocketMQ 訊息軌跡MQ
- 【RocketMQ】訊息拉模式分析MQ模式
- 訊息佇列之RocketMQ佇列MQ
- 【RocketMQ】訊息的拉取MQ
- 【RocketMQ】訊息的儲存MQ
- 【RocketMQ】MQ訊息傳送MQ
- RocketMQ 常用訊息型別MQ型別
- 訊息佇列之 RocketMQ佇列MQ
- RocketMQ(八):訊息傳送MQ