背景
我們項目本身分成了多套系統,但數據上有要求一致性的地方(比如訂單狀態,通俗點講就是系統A更新了訂單狀態為狀態一,那么系統B也需要把相同訂單的訂單狀態更新成狀態一,這樣可以讓我們不管是讀系統A還是系統B的同一個訂單的狀態,都可以讀出相同的狀態數據)。
最原始的偽代碼大概為:
sysA.db.transaction.begin();
var success = sysA.db.transaction.updateOrderStatus();
if(!success){
sysA.db.transaction.rollback();
return;
}
success = httpclient.post(sysB.url);
if(!success){
sysA.db.transaction.rollback();
return;
}
sysA.db.transaction.commit();
看了這個代碼后,大家應該知道這里有個問題是調用系統B接口,可能出現網絡或超時等問題,所以你是不知道系統B是執行成功了還是失敗,如果他是執行成功了的,那么系統A回滾了就出問題了,因為兩邊數據就不一致了。
那么基於CAP理論的實現版BASE,我們采用數據最終一致方案。關於CAP跟BASE,大家可以參考這里
然后我這里參考了個別文章跟同事想法,思考了其中兩個解決方案
簡單解決方案一
異步任務處理,由原來同步調用B系統變為異步調用。
-
系統A先寫數據庫更新訂單狀態,然后在本地同時新建一個任務(任務初始狀態為待執行),當調用B系統接口完成之后該任務改為已經執行,修改訂單狀態跟建立任務需要在一個數據庫事務下。
-
后台定時腳本來執行待執行狀態的任務。
-
如果異步調用系統B接口返回失敗,則需要對之前訂單狀態更新進行回退。
-
如果異步調用系統B接口遇到網絡問題或者超時,則考慮重試機制,注意重試機制要避免重復提交,可采取在系統A重試前確認和在系統B保證接口的冪等。
想了一下,這個方案有個問題就是狀態可能發生了多次改變,如果先后順序出現問題,那么將造成訂單狀態更新錯誤,所以就得有個隊列來保存訂單狀態的多次先后順序更新才行,於是有了方案二
簡單解決方案二
引入消息隊列,相當於對方案一的升級版,新建立任務變成新建立消息
-
系統A為消息生產者,系統B為消息消費者。
-
生產者系統A接收到用戶請求,先寫數據庫更新訂單狀態,然后寫一條更新訂單狀態的消息到消息隊列,並且要新建一張消息狀態表用於記錄消息的執行狀態(初始狀態為待執行),以上三個操作要在同一個本地事務中進行,這個是容易忽略的地方。其實也可以在業務訂單表增加一個字段用來表示消息執行狀態。當然對於這個訂單的后續業務操作,只有在這個消息執行成功后才能繼續,也就是有一個因果先后關系在里面才行。
-
消費者系統B取出一條消息,進行相同訂單的狀態更新,處理完成后需要告訴系統A消息執行結果,是成功還是失敗。這里簡單說下失敗的情況,第一次失敗的話,系統B就可以進入重試邏輯,重試多次如果還是失敗才需要告訴系統A我這邊最終執行失敗了,你可能要采取點措施才行,比如回滾呀,還是什么的。
總結
- 分布式系統以基本可用跟快速高效的最終一致為目標
- 遠程RPC調用中的網絡跟硬件等問題是造成一致性的主因,異步解耦加消息隊列等方式可作為分布式系統滿足最終一致性的一個比較好的方案。
參考: