一致性問題,「萬惡之源」是數據冗餘和分佈並經過網絡交互+網絡異常是常態。java
所謂分佈式服務,就是把以前經過本地接口交互的模塊,拆分紅單獨的應用獨立部署,並經過遠程接口和網絡消息交互。且無論這樣說嚴不嚴密,正不正確,理解就好。本文的重點也不是這個話題。簡單畫一張圖輔助理解,如圖。集中式架構,要想保證訂單表和庫存表的一致性,只要一個本地事務(ACID)就能保證二者的強一致性。分佈式架構,訂單表由訂單服務操做,庫存表由庫存服務操做。要想保證訂單表和庫存表的一致性,那麼就必須保證訂單服務對訂單表的操做和庫存服務對庫存表的操做同事成功。以前的一個本地事務就變成了一個分佈式事務。因爲服務之間經過網絡交互+網絡異常是常態,就會產生服務間數據不一致的狀況。這就涉及一個分佈式事務一致性的問題。數據庫
模式分析:A服務同步調用B服務的接口並等待結果返回,後續的流程會依賴B服務的返回結果。這種交互模式下,A服務獲得的結果細分有三種狀況。apache
業務場景:適用於大規模、高併發的短小操做且依賴返回值的場景。例如,交易服務和庫存服務(卡券服務、紅包服務等)的交互、用戶登陸和准入服務的交互等。緩存
解決方案:方案一,服務調用方查詢重試方案;方案二,TCC方案。服務器
注:這兩種方案,保證數據一致性實際上仍是靠「異步」,只不過須要快速校準,準實時。markdown
服務調用方查詢重試方案,適合一個從業務服務場景。下單減庫存方法() { // 1.準備操做 // 2.重試調用B服務 result = RetryUtil { while(重試次數 < 最大重試次數) { try { if (重試次數 != 0) { // case1:網絡超時或異常(catch分支) // case2:查詢到扣減庫存操做,result=成功(return) // case3:查不到扣減庫存操做,result=失敗(繼續下面操做) result = rpc.查詢扣減庫存是否成功(); if (result == 成功) { return result; } } // case1:網絡超時或異常(catch分支) // case2:扣減庫存成功,result=成功(return) // case3:扣減庫存失敗,result=失敗(return) return rpc.扣減庫存(); } catch (Exception e) { if (重試次數 = 最大重試次數) { // 報警,人工處理或者(近實時)對帳系統自動校準 // 拋出異常,中斷後續流程 throw 自定義異常; //或者result封裝異常 } } } }; // 3.後續操做 }複製代碼
注:1) 查詢重試後依然失敗(極少),報警,人工處理或者準實時對帳系統自動校準;網絡
2) 重試次數不宜多,甚至只重試一次;架構
3) B服務處理請求要作冪等。併發
模式分析:A服務調用B服務,B服務先受理請求並落庫,狀態是待處理。B服務處理請求很耗時,或者要依賴其餘的服務。B服務處理完後通知A服務或者A服務定時去查詢B服務的處理結果。這種交互模式下,對於CASE-1,第1步和第2步同接口同步調用模式,第3步同消息異步處理模式;對於CASE-2,至關於兩次接口同步調用模式。
異步
業務場景:適用於非核心鏈路上負載較高的處理環節,這個環節常常耗時較長,而且對時效性要求不高。例如,用戶提現時,帳戶系統和提現系統的交互(CASE-1);提現系統和三方系統(銀行系統或者三方託管系統)的交互(CASE-2)。
解決方案:服務被調方最大努力處理方案。因爲B服務中請求有落庫,因此能夠用定時任務不斷重試盡最大努力將請求處理出結果。處理後,將請求狀態設置成對應的結果落庫。而後再通知A服務或者A服務異步主動查詢。
受理請求方法() { // 1.請求落庫,狀態爲待處理 // 2.返回受理結果 if (落庫成功) { // 返回受理成功 } else { // 返回受理失敗 } } 定時任務處理請求方法() { // 1.掃描待處理請求 try { // 2.處理請求 if (處理成功) { // 設置請求處理狀態爲處理成功 } eles { // 設置請求處理狀態爲處理失敗 } } catch (Exception e) { // 不作任何操做,請求狀態依舊爲待處理 } // 3.消息通知A服務處理結果或者等待A服務查詢處理結果 }複製代碼
注:1) B服務一般都是接受請求並持久化後才返回A服務受理成功。避免服務進程被殺掉而致使請求丟失。
2) 不論是第(1,2)兩步仍是CASE-2中的第(3,4)兩步,若是查詢重試失敗,能夠落庫,用定時任務處理,知道成功。反正不像接口同步調用模式,A服務不須要實時的結果。
模式分析:A服務將B服務須要的信息經過消息中間件傳遞給B服務,A服務無需知道B服務的處理結果。這種交互模式下,消息生產者要確保消息發送成功;消息消費者要確保消息消費成功。
業務場景:消息異步處理模式與接口異步調用模式相似,多應用於非核心鏈路上負載較高的處理環節中,井且服務的上游不關心下游的處理結果,下游也不須要向上遊返回處理結果。例如,在電商系統中,用戶下訂單支付且交易成功後,發送消息給物流系統或者帳務系統進行後續的處理。
解決方案:生產者最大努力通知+消費者最大努力處理方案。
交易完成發消息方法() { // 1.設置交易狀態爲已完成 // 2.消息落庫,狀態爲待發送 // 可異步發送,也建議異步發送 try { // 3.發送消息 if (發送成功) { // 設置消息狀態爲發送成功 } } catch (Exception e) { // 不作任何操做,消息狀態依舊爲待發送 } } 定時任務發送消息方法() { // 1.掃描待發送消息 try { // 2.發送消息 if (發送成功) { // 設置消息狀態爲發送成功 } } catch (Exception e) { // 不作任何操做,消息狀態依舊爲待發送 } }複製代碼
注:1) 定時任務重試發送消息和消息服務器重發未ACK的消息通常都是時間階梯式的(2n*時間間隔);
2) 支持事務消息中間件之RocketMQ。
注:保證冪等性的方法不少,根據具體的業務場景,總能找到保證冪等性的方法。