前言
rocketmq 消息隊列在大部分業務系統中都會用到(如下圖),服務A和服務B系統通過mq進行業務解耦;服務A處理完業務之後,發一條消息到rocketmq,服務B從rocketmq拉去消息進行消費。
上面這個流程會有下面幾個問題:
1.如果服務A寫數據庫成功了,還沒來得及發送消息就宕機了,這就會導致服務B永遠消費不了消息,出現了數據不一致問題;
2. 服務A寫數據庫成功,發送mq消息失敗,也會出現服務B消費不了消息的情況;
3.服務A寫數據庫成功,發mq消息成功了,但是服務A事務由於異常回滾了,服務B消費了消息。
針對上面的幾個問題, 我們在生產開發中如何解決呢?
1.重試+回滾
針對第二個問題,我們可以使用重試加回滾的方式解決;先寫本地事務,然後發送mq消息,如果發送失敗再重試幾次,重試也是失敗就回滾本地事務。
public void doService() {
//執行本地事務
server.doLocalTransaction();
for (int i = 0; i < 3 ; i++) {
try {
producer.send(message);
return;
} catch (Exception e) {
continue;//繼續重試
}
}
server.rollback();
}
缺點:本地事務和發送消息不在一個事務裏,如果mq重試也失敗後系統宕機了,服務A事務還是無法回滾。
2.事務+重試回滾
針對上面服務A事務無法回滾的問題,我們可以將發送消息流程加到整個事務裏來,出現異常事務會自動回滾。
@Transactional
public void doService() {
//執行本地事務
server.doLocalTransaction();
for (int i = 0; i < 3 ; i++) {
try {
producer.send(message);
return;
} catch (Exception e) {
continue;//繼續重試
}
}
throw new BusinessException("rollback....");
}
缺點:如果出現網絡異常再重試發送消息,會導致數據資源被鎖定很長時間,影響數據庫的併發性能,同時也響應系統的性能。
3.補償機制
服務A發送消息失敗什麼也不處理,j可以將數據狀態更改爲發送消息失敗,補償任務每五分鐘掃描一次數據庫,發現有發送消息失敗的數據則重新發送mq消息,將數據狀態更改爲成功。
public void doService() {
//執行本地事務
server.doLocalTransaction();
try {
producer.send(message);
return;
} catch (Exception e) {
server.updateFail()
}
server.rollback();
}
ScheduledThreadPoolExecutor executor=new ScheduledThreadPoolExecutor();
executor.scheduleAtFixedRate(()-> server.checkLocalTransaction(),0L,5, TimeUnit.MINUTES);
缺點:這個方案有點依賴數據庫做狀態判斷。
4.rocketmq 事務消息
使用rocketmq 事務消息,解決發送消息和本地事務不一致性問題:
1.服務A向mq發一條prepare消息;
2.發送失敗說明mq不可用,直接回滾事務返回失敗;
3.發送成功執行本地事務,向mq提交事務狀態;
4.如果服務A宕機了沒有向mq提交事務狀態;服務A(單機部署)恢復之後,mq會回查事務結果;
5.事務處理成功,mq會將消息可見,服務B拉取並消費消息。
這個方案能很好的解決發送消息和本地事務一致性的問題,相對上面其他幾個方案,會好很多。不過在生產中可以根據具體情況進行方案選取。