實戰與原理:如何基於RocketMQ實現分散式事務?
來源:JAVA日知錄
今天,我們將深入討論在DailyMart中如何使用RocketMQ的事務訊息以及事務訊息的底層原理。
使用事務訊息
在DailyMart系統中,使用者發起支付後,訂單系統需要呼叫庫存服務執行庫存扣減邏輯。
由於這是跨服務呼叫,因此會產生分散式事務。在這裡,我們使用RocketMQ的事務訊息來實現分散式事務。
1、首先,在訂單服務的應用服務層處理支付邏輯,並呼叫RocketMQ傳送事務訊息:
@Override
public String payment(String orderSn) {
// todo 整合支付寶支付
// 支付流水號
String outOrderNo = IdUtils.get32UUID();
TradeOrder tradeOrder = Optional.ofNullable(tradeOrderService.getByOrderSn(orderSn)).orElseThrow(() -> new BusinessException("訂單編號不存在"));
// 如果訂單處於待支付狀態
if (Objects.equals(tradeOrder.getStatus(), OrderStatusEnum.WAITING_PAYMENT.getStatus())) {
OrderPaidEvent orderPaidEvent = new OrderPaidEvent(orderSn, outOrderNo);
TransactionSendResult sendResult = enhanceTemplate.sendTransaction("TRADE-ORDER", "ORDER-PAID");
if (SendStatus.SEND_OK == sendResult.getSendStatus() && sendResult.getLocalTransactionState() == LocalTransactionState.COMMIT_MESSAGE) {
return tradeOrder.getOrderSn();
} else {
throw new BusinessException("支付失敗...");
}
} else {
throw new BusinessException("訂單已支付,請勿重複提交...");
}
}
2、在訂單服務的基礎設施層,建立一個類實現 RocketMQLocalTransactionListener 介面:
該介面有兩個方法:
executeLocalTransaction
:用於執行本地事務。checkLocalTransaction
:在RocketMQ執行訊息回查時檢查本地事務執行結果,用於確定訊息提交還是回滾。
@Component
@Slf4j
public class OrderPaidTransactionConsumer implements RocketMQLocalTransactionListener {
@Resource
private TransactionTemplate transactionTemplate;
@Resource
private TradeOrderService tradeOrderService;
/**
* 執行本地事務
* 將訂單狀態修改成已支付
*/
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o) {
final OrderPaidEvent orderPaidEvent = JsonUtils.byte2Obj((byte[]) message.getPayload(), OrderPaidEvent.class);
try {
// 放到同一個本地事務中
this.transactionTemplate.executeWithoutResult(status -> {
String orderSn = orderPaidEvent.getOrderSn();
// 修改成待發貨
tradeOrderService.changeOrderStatus(orderSn, OrderStatusEnum.AWAITING_SHIPMENT);
});
return RocketMQLocalTransactionState.COMMIT;
} catch (Exception e) {
log.error("修改訂單狀態失敗", e);
// ROLLBACK 則回滾訊息,rocketmq將廢棄這條訊息
return RocketMQLocalTransactionState.ROLLBACK;
// 如果是UNKNOWN, 則觸發回查
}
}
/**
* 檢查本地事務執行狀態
* 訊息回查時,對於正在進行中的事務不要返回Rollback或Commit結果,應繼續保持Unknown的狀態。
*/
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
final OrderPaidEvent orderPaidEvent = JsonUtils.byte2Obj((byte[]) message.getPayload(), OrderPaidEvent.class);
String orderSn = orderPaidEvent.getOrderSn();
TradeOrder tradeOrder = tradeOrderService.getByOrderSn(orderSn);
// 如果已經修改成待發貨說明本地事務執行成功,此時消費端可以直接消費
if (Objects.equals(tradeOrder.getStatus(), OrderStatusEnum.AWAITING_SHIPMENT.getStatus())) {
return RocketMQLocalTransactionState.COMMIT;
} else {
// 這裡查不到的時候返回 UNKNOWN在於,有可能事務還沒有提交,回查就開始了
return RocketMQLocalTransactionState.UNKNOWN;
}
}
}
3、在庫存服務的基礎設施層,監聽訊息以執行庫存扣減邏輯:
@Component
@Slf4j
@RocketMQMessageListener(consumerGroup = "dailymart_inventory_group", topic = "TRADE-ORDER", selectorExpression = "ORDER-PAID")
public class InventoryDeductionConsumer extends EnhanceMessageHandler<OrderPaidEvent> implements RocketMQListener<OrderPaidEvent> {
@Resource
private InventoryDomainService inventoryDomainService;
@Override
public void onMessage(OrderPaidEvent orderPaidEvent) {
super.dispatchMessage(orderPaidEvent);
}
@Override
protected void handleMessage(OrderPaidEvent orderPaidEvent) throws Exception {
// 執行庫存扣減邏輯
String orderSn = orderPaidEvent.getOrderSn();
inventoryDomainService.deductionInventory(orderSn);
}
}
透過以上步驟,我們完成了RocketMQ事務訊息的傳送,利用事務訊息的特性保證分散式事務的最終一致性。與普通訊息相比,事務訊息在處理時需要實現 RocketMQLocalTransactionListener 介面,這是事務訊息的核心。
介紹完事務訊息的使用,接下來我們再來聊聊事務訊息的原理。
事務訊息的原理
首先,讓我們思考一下,如果不使用事務訊息會有什麼問題。
很容易想到的一個問題就是訊息丟失。當儲存訂單後由於網路問題導致訊息丟失,如下圖所示:
在不使用RocketMQ的情況下,我們往往會透過 本地訊息表 + 補償重試
的機制來保證訊息一定會傳送出去。其原理可以參考上篇文章 [Dailymart26:微服務中躲不過的坑 - 分散式事務]。
那RocketMQ是如何解決這個問題的呢?
1. 傳送half訊息,探測MQ是否正常
在基於RocketMQ的事務訊息中,我們不是先執行自身的訂單支付邏輯,而是先讓訂單系統傳送一條 half訊息 到MQ去。這個half訊息本質上是一個訂單支付成功的訊息,只不過此時庫存系統是看不見這個half訊息的。然後,我們等待接收這個half訊息寫入成功的響應通知。
傳送half訊息的本質其實是為了探測MQ是否仍然正常執行。但問題來了,如上所述,訊息會發生丟失,那麼half訊息丟失怎麼辦呢?
2. half訊息傳送失敗
在傳送half訊息時,由於網路原因或者MQ直接掛了,就會導致half訊息傳送失敗。這個時候訂單系統需要執行一系列的回滾操作。在我們的場景中,應該執行退款操作,將錢退還給使用者,並告知使用者交易失敗。
3. half訊息成功,訂單系統執行自己的業務邏輯
如果成功收到half訊息的正常響應,此時訂單系統應該執行自己的業務邏輯。在我們這個場景中,就是修改訂單資料庫狀態,將其修改為待發貨狀態。這部分邏輯就對應上述程式碼中的executeLocalTransaction()
方法。
4. 訂單本地事務執行失敗
如果訂單系統執行本地事務失敗,則需要傳送一個rollback請求給MQ,讓其刪除這條half訊息。
5. 訂單本地事務執行成功
如果訂單系統的本地事務執行正常,此時需要傳送一個commit請求給MQ,要求MQ對之前的half訊息進行commit操作,這樣庫存系統就可以消費這條訊息了。
訂單建立訊息處於half狀態時,庫存系統是看不見它的。必須等到訂單系統執行commit請求,訊息被commit後,庫存系統才能看到並獲取這條訊息進行後續處理。
6. half訊息傳送成功,但是沒收到half的響應
以上就是RocketMQ事務訊息的正向流程。
然而,還有一個問題:如果訂單系統傳送half訊息成功後卻沒有收到half訊息的響應,該如何處理呢?
在這種情況下,訂單系統可能會誤以為是傳送half訊息到MQ失敗了。訂單系統就會執行回滾流程,退還支付金額,關閉訂單。
然而,此時MQ系統中已經存在了一條half訊息。這條half訊息又該如何處理呢?
在RocketMQ中,有一套補償流程。RocketMQ會定期掃描處於half狀態的訊息。如果一直沒有對這個訊息執行 commit/rollback
操作,超過了一定的時間,RocketMQ就會回撥你的訂單系統的一個介面,用以確認你本地事務的情況。
當訂單系統收到MQ的回查請求時,就需要檢索一下資料庫,根據訂單狀態決定執行commit還是rollback。
這部分邏輯就對應上述程式碼中checkLocalTransaction()
方法。
7. rollback 或者 commit 失敗怎麼辦?
透過上述說明,可以看到,RocketMQ是根據rollback或commit操作來決定half訊息的狀態的。如果業務系統執行了commit操作,則將half訊息設定為可見,庫存系統可以消費;如果業務系統執行了rollback操作,MQ就會刪除half訊息。那麼問題來了:如果訂單系統在執行rollback或commit操作時失敗又該如何處理呢?
這時候仍然依賴於前文提到的回查機制。
由於此時MQ中的訊息一直處於half狀態,超過一定的超時時間後,MQ會發現這個half訊息有問題,然後回撥你的訂單系統的介面。此時訂單系統需要根據訂單狀態來決定執行commit請求還是rollback請求。
以上,就是RocketMQ事務訊息的原理。結合文章開頭的程式碼,是不是已經很清晰了呢?
來自 “ ITPUB部落格 ” ,連結:https://blog.itpub.net/70024922/viewspace-3005468/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 基於RocketMQ實現分散式事務MQ分散式
- 分散式事務(3)---RocketMQ實現分散式事務原理分散式MQ
- 分散式事務(4)---RocketMQ實現分散式事務專案分散式MQ
- SpringCloud+RocketMQ實現分散式事務SpringGCCloudMQ分散式
- 分散式事務之JTA原理與實現(三)分散式
- 分散式事務實戰分散式
- Laravel基於reset機制實現分散式事務Laravel分散式
- MySQL 中基於 XA 實現的分散式事務MySql分散式
- 構建基於RocketMQ的分散式事務服務MQ分散式
- 基於RocketMq的分散式事務解決方案MQ分散式
- php基於dtm分散式事務管理器實現tcc模式分散式事務demoPHP分散式模式
- go-zero微服務實戰系列(十、分散式事務如何實現)Go微服務分散式
- MassTransit | 基於StateMachine實現Saga編排式分散式事務Mac分散式
- 分散式事務(八)Spring Cloud微服務系統基於Rocketmq可靠訊息最終一致性實現分散式事務分散式SpringCloud微服務MQ
- Spring Cloud Seata系列:基於AT模式實現分散式事務SpringCloud模式分散式
- 基於Seata探尋分散式事務的實現方案分散式
- Apache ShardingSphere 如何實現分散式事務Apache分散式
- 分散式事務與Seate框架(3)——Seata的AT模式實現原理分散式框架模式
- 資料庫分散式事務的實現原理!資料庫分散式
- 分散式鎖與實現(一)基於Redis實現!分散式Redis
- RocketMQ在基金大廠的分散式事務實踐MQ分散式
- 分散式事務之資料庫事務與JDBC事務實現(一)分散式資料庫JDBC
- MassTransit 知多少 | 基於MassTransit Courier實現Saga 編排式分散式事務分散式
- 分散式事務理論加實戰分散式
- memcached分散式原理與實現分散式
- 分散式事務之事務實現模式與技術(四)分散式模式
- RocketMQ 分散式事務訊息MQ分散式
- 分散式訊息佇列RocketMQ--事務訊息--解決分散式事務的最佳實踐分散式佇列MQ
- MySQL資料庫分散式事務XA的實現原理分析MySql資料庫分散式
- 分散式鎖實現原理與最佳實踐分散式
- 使用Spring Boot實現分散式事務Spring Boot分散式
- 基於微服務框架Micronaut和Eventuate Tram實現分散式事務的開源案例微服務框架分散式
- 分散式資料庫事務故障恢復的原理與實踐分散式資料庫
- 基於redis實現分散式鎖Redis分散式
- 基於ZK實現分散式鎖分散式
- 我是如何基於二階段遞交及悲觀鎖實現分散式事務的分散式
- 如何實現跨Mysql、Redis和Mongo分散式事務? - dongfuMySqlRedisGo分散式
- Redis、Zookeeper實現分散式鎖——原理與實踐Redis分散式