下文是為應對我司分布式事務帶來的問題所設計的解決方案,已在生產環境應用,由于之前是word版本寫的就直接copy來了,其中一些代碼和命名不用太在意,可以提供給大家參考的主要是設計和實現思路,當然,這也是基于公司現有業務和本人設計水平而決定的,一定是不完美甚至是不完善的,不足指出,歡迎指正。
一 背景
??庫存系統現進行改進優化,目標將以前所有與庫存有直接關系的數據庫表逐漸拆離出去。將以前各系統對這些表的訪問,變為訪問遠程服務。遠程服務由庫存系統carInventory提供。如下圖如示(以ordercollect表為例):無論“原先的方式”,還是“現在的方式”我們的業務系統程序流程都是同步執行的。
二 問題
??由于采用遠程服務調用方式執行程序,程序的穩定和可靠性相對降低,風險加大,比如:
- 請求及響應過程中網絡異常(包括超時,未響應等)
- 調用及被調用端程序異常
- 其它影響程序穩定性的異常
三 問題分析
??這里我們將整個的調用過程中的調用方稱為系統A,將被調用方稱為子系統B。子系統B中封裝了遠程調用的方法,其中針對數據庫表的操作是帶事務的,如在子系統B的程序執行過程中出現了異常,因為有事務,所以會回滾異常數據。相對的,在系統A中在程序的執行過程中,也是帶事務執行的,所以A、B兩端分別有兩個事務。
盡管在兩端我們都有事務,可還是會在一些節點出現問題,如下圖所示,是兩系統RPC的交互過程。
??因為兩系統的事務解決了兩端程序的異常問題(回滾),所以這里我們重點關注的是請求響應異常的情況:
如下圖所示:在請求返回異常結果的時候會出現以下兩個問題:
- 一次請求不成功,有可能子系統B的數據已經入庫,只是可能由于網絡原因,或其它原因,導致返回結果異常。如果進行多次重試,每一次重試,由于子系統B的程序都能執行成功,將會產生贓數據。只不過還是返回的過程中出現異常
-
系統A在所有請求全部失敗的情況下,如何回滾子系統B可能已經產生的數據。
image.png
四 解決方案
??如果系統在調用過程中一切正常,無論在任何節點,任何一段程序代碼都不會出現問題,我們就不用考慮了。然而實際的情況是,系統有可能會在各種節點產生不一樣的問題,雖然出現問題的概率比較小,但也需要我們準備好相應的解決方案來處理。
??由于之前我們已分析出問題可能出現的位置,所以下面來說明一下針對這些問題的解決辦法。
??理論上,我們是可以將這兩個事務做成分布式事務的,目前對于不要求強一致性的業務,比較流行的做法是將分布式事務,拆解為異步消息通知方式,通過消息隊列處理任務,做到最終一致性。但由于我們的業務系統中的業務流程是同步的,而非異步的,而且庫存計算結果是要求比較強的一致性和低延遲的效果,所以不能完全使用這個方法。如果使用分布式事務(深坑),又會增加復雜度和維護成本,所以這里也不建議采用分布式事務。
??綜合考慮,我們的方案是:RPC重連機制+構造冪等接口+事務補償機制
1 PRC 重連機制
??在各系統中通過maven引用的框架包中,關于RPC調用默認采用的是Hession,包中已經幫我們封裝好了超時及重試。超時是在配置中心配置,如下圖所示:
重試,則是通過傳遞自定義參數來進行的,如下代碼所示:
//自定義參數方式
RemoteClientContextVO vo = new RemoteClientContextVO();
vo.setRepeatCount(5);
vo.setRemoteType(RemoteType.HESSIAN);
vo.setUrl("http://localhost:8096/carinventory");
remoteClient = RemoteClientFactory.getInstance(vo);
上面代碼中repeatCount屬性即為重試次數。重試的原理,如下圖:2冪等接口構造
??有關API的冪等性,簡單說,就是對同一接口,調用多次和調用一次的情況一樣。舉例說明:對于ordercollect表的insert操作,如果第一次請求子系統B執行成功但返回異常,那么系統A將發起重試請求,這時子系統B,不會再進行數據庫的insert操作,而是直接返回上一次的執行結果。
??具體做法是:我們將請求參加中,多加一個requestid參數,這個參數可以是uuid,即一次請求的唯一標識。每次請求都會帶著這個requestid來進行請求。在子系統B中,我們用redis來存儲requestid以及相對應的返回值,結構如下:<uuid,返回結果對象>,這樣在每次請求的時候,都會先查詢redis有沒有與requestid相匹配的鍵值,如果有,直接返回,如果沒有再進行程序流程。Redis由于我們具有主備,可以不用擔心單點問題,redis中的數據,會定時進行一次清理(可能每天)。
3事務補償機制
??此處我們來解決問題分析中的第二個問題,即系統A如何回滾子系統B的數據。由于數據庫的DML中我們只關心insert、update和delete,所以解決方案也是圍繞這些操作設計的。
拿orderCollect表來舉例。
- 我們在orderCollect表中建議一個requestid字段,用來存儲請求標識。
- 在系統A端加入消息隊列,當系統A請求子系統B異常時,往消息隊列中發一條消息,意為告知子系統B異步處理異常,即回滾異常數據。消息內容比較簡單,即requestid+請求類型(insert,upate)。
- 子系統B通過消息隊列異步得到消息時,根據requestid去ordercollect表中查詢字段requestid符合當前請求值的數據記錄。
-
根據請求類型(由于orercollect表沒有物理刪除操作,所以目前只考慮insert和update兩種情況),對相應的數據進行回滾處理。
--1) 如是insert,則刪除相應的數據
--2) 如是update,相對復雜些。首先,我們在子系統B每一次執行update操作的時候都會將數據copy一份存在一張表中。當消息過來要求回滾時,我們將原數據庫表和copy數據表進行連表更新,即將copy的原數據覆蓋回原表。Sql語句類似這樣:
image.png
綜上所述,事務補償這部分如下所示: