摘要
如果你選用的消息中間件事rocketMQ,可以直接使用它的事務消息方便的實現分佈式事務。但如果你用的是rabbitMQ或kafka這些沒有事務消息的中間件,就需要自己來設計分佈式事務的實現方案了。一種經典的方案是本地消息表,通過本地事務+定時任務來實現最終一致。本文介紹另一種方案,通過自己實現可靠性消息服務,來保證消息發佈方和消費方的最終一致。
方案設計
該設計方案來自於石杉架構筆記.
1,上游服務發送消息到可靠消息服務
上游服務告訴可靠消息服務保存一條待確認的消息,然後就執行業務邏輯操作本地數據庫,如果業務操作成功就告訴可靠消息服務更新消息爲“已發送”並投遞消息到MQ;如果業務操作失敗了,就告訴可靠消息服務刪除之前的“待確認”狀態的消息。
如果操作本地數據庫成功了(步驟3),但通知可靠消息服務更新消息狀態(步驟4)時失敗了怎麼辦?
如果發生了這種情況,可靠消息服務中的消息會繼續保持“待確認”的狀態下去,相當於消息丟了,也沒有別的影響。如果想做的更好些,可以在可靠消息服務開一個後臺線程運行定時任務,檢查狀態爲“待確認”且已過期的消息,詢問上游服務相關業務操作是否成功了,如果成功就更新消息狀態爲“已發送”同時投遞消息到MQ;否則刪除消息。
2,可靠消息服務投遞消息到MQ
在可靠消息服務中更新消息爲“已發送”和將消息投遞到MQ這兩個操作要放到一個本地事務中,保證:
- 如果數據庫裏更新消息的狀態失敗了,那麼就拋異常退出了,就別投遞到MQ;
- 如果投遞MQ失敗報錯了,那麼就要拋異常讓本地數據庫事務回滾。
這倆操作必須得一起成功,或者一起失敗。
3,下游服務消費MQ消息
下游服務消費MQ消息後,執行業務操作,如果成功了告知MQ消費成功,並告知可靠消息服務更新消息狀態爲“已完成”。
那如果下游服務消費消息出了問題,沒消費到?或者是下游服務對消息的處理失敗了,怎麼辦?
仍然可以在可靠消息服務後臺開一個線程執行定時任務,檢查狀態爲“已發送”且超時的消息,再次將此消息投遞到MQ讓下游消費,需要下游保證消費消息的冪等性。
業務場景
現在有電話營銷系統(後面簡稱電銷系統)和調度平臺兩個系統。電銷系統處理具體電銷案件作業邏輯,依賴於調度平臺將案件分配給人處理。電銷系統中有marketing_task表維護電銷案件,調度平臺有dispatching_task表維護包括電銷系統、催收系統、審批系統等業務系統在內的多種業務案件。
電銷系統生成電銷案件後,是通過MQ中間件將案件信息推送調度平臺,後者異步將案件保存並完成後續分配。
下面我們就來看下如何使用可靠消息服務來保證電銷系統和調度平臺之間,案件狀態的最終一致。
方案實踐
首先,定義可靠消息服務接口
public interface ReliableMsgService {
// 保存一條消息,狀態位“待確認”,消息體包含上游要發送給下游的信息
public ReliableMsgResponse prepare(ReliableMsgRequest request);
// 供上游數據庫事務成功時調用,消息確認做兩件事:1,更新消息狀態爲“已發送”;2,將消息內容投遞到MQ中間件。
public ReliableMsgResponse confirm(ReliableMsgRequest request);
// 供上游數據庫事務失敗時調用,刪除消息
public ReliableMsgResponse delete(ReliableMsgRequest request);
// 供下游在消息消費成功後調用,更新消息狀態爲“已完成”
public ReliableMsgResponse finish(ReliableMsgRequest request);
}
注意接口實現的confirm方法要定義本地事務,保證兩件事同時成功或失敗.
在電銷推送案件到調度的業務場景中,電銷作爲上游系統與可靠消息服務交互如下:
public class TaskService{
@Autowired
private ReliableMsgService reliableMsg;
public void pushTask(String caseNo){
// 省略將消息體放入request代碼
...
try{
// 發送待確認消息
reliableMsg.prepare(request);
}catch(Exception e){
log.error('調用可靠消息服務prepare出現異常',e)
return;
}
try{
// 更新電銷案件表,將案件設爲處理中
updateTaskStatus(caseNo, HANDLING);
// 更新成功,發送確認消息
reliable.confirm(request);
}catch(Exception e){
// 更新出現異常,發送刪除消息
reliable.delete(request);
}
}
}
作爲下游服務的調度平臺消費MQ消息邏輯如下:
public void CreateTaskConsumer{
@Autowired
private ReliableMsgService reliableMsg;
public void consume(Message msg){
// 生成電銷案件
insertDispachingTask(msg);
// 此處省略準備request代碼
...
// 通知可靠消息服務,消息處理完畢
reliableMsg.finish(request);
}
}
上面的都是主流程中的代碼,我們再看一下出現異常時的處理,主要是方案裏提到的可靠消息服務中定義的兩個定時任務。
定時任務1
// 查詢“待確認”且超時的消息
public class HandleOverTimePrepareJob {
@Autowire
private TaskStatusQueryFacade taskStatusQuery;
@Autowire
private ReliableMsgService reliableMsg;
public void execute(JobContext context){
// 查找狀態爲“待確認”同時超時了的消息
List<RMessage> list = queryOverTimePrepareMsg();
for(RMessage msg:list){
Boolean res = taskStatusQuery.updateSucceed(msg.caseNo);
//省略準備request代碼
...
if(res){
reliableMsg.confirm(request);
}else{
reliableMsg.delete(request);
}
}
}
}
對應電銷系統接口定義:
// 電銷系統接口
public interface TaskStatusQueryFacade{
// 電銷系統更新案件狀態是否成功
public Boolean updateSucceed(String caseNo);
}
定時任務2
// 查詢“已發送”且超時的消息
public class HandleOverTimeConfirmJob {
@Autowire
private ReliableMsgService reliableMsg;
public void execute(JobContext context){
// 查找狀態爲“已發送”同時超時了的消息
List<RMessage> list = queryOverTimeConfirmedMsg();
for(RMessage msg:list){
//省略準備request代碼
...
// 再次投遞消息到MQ
reliableMsg.confirm(request);
}
}
}
總結
對比本地消息表方案可以看出,可靠消息服務方案其實就是把消息表的維護從發送方的數據庫獨立出來作爲單獨的服務。優點是可以使上游系統更加專注於業務代碼。其實RocketMQ中的事務消息的思路與本文描述的方案類似,如果你使用的是RocketMQ直接使用事務消息更方便,如果是其他不支持事務消息的MQ中間件,可以考慮本方案。