一開始我想着是在Feign的ErrorDecoder上做自定義的異常處理,來實現根據http code拋出各種異常。但是Feign與Hystrix結合之后,發現一個問題,只要服務調用拋出了Throwable類就會觸發Hystrix的fallback(前提是配置了fallback)。想來想去都沒有想到怎么利用這套機制來實現業務邏輯上的異常分支和服務器處理異常。最后,靈光一現:
我把異常分成兩大類,ClientException和ServerException。無論哪一種異常,都會導致對應服務本地事務回滾。當發生ClientException時,向客戶端返回的http code為200,當發生ServerException時,向客戶端返回的http code為500。
ClientException是由客戶端引起的異常,比如輸入非法參數或者用戶登錄輸入錯誤的密碼導致登錄失敗而業務邏輯進入到非主線分支,這類問題其實都是客戶端導致的,只需要拒絕服務讓客戶端提交正確的參數即可恢復。因此當遇到這種異常的時候,其實業務邏輯是執行成功的,只不過進入了非主線分支而已。
ServerException則是由服務器上的邏輯漏洞或者其他什么原因導致的無法由客戶端重新提交來糾正的錯誤,比如服務器代碼上寫的不嚴謹在某個地方報了空指針異常,這種問題客戶端無論提交多少次正確的參數都無法修正,因此是屬於服務器內部錯誤。
將異常分成這兩類之后,處理起來就清晰很多了。現在假設有兩個服務A、B,外部請求進來的時候,由網關服務轉發至A,A的處理過程中需要調用B,此時可能會發生以下幾種情況:
1、A調用B之前發生ClientException,以Http Code 200返回結果至外部客戶端,http Body中會有固定的格式包含自定義的業務code、msg、data等。客戶端解析code后提示用戶msg並執行對應的邏輯來重新提交請求。通俗一點講就是登陸的時候輸入了錯誤的密碼,請求提交后頁面提示密碼錯誤,此時重新輸入正確密碼,提交,登錄成功。
2、A調用B之前發生ServerException,這個沒辦法,服務暫時不可用了。等重新發布來修復。
3、A調用B時,B發生ClientException。此時Http Code 200返回結果至A,但是A在從http Body中提取data時,會發現業務code不等於操作成功的那個值,則A拋出對應的ClientException,讓客戶端重新提交正確參數。
4、A調用B時,B發生ServerException。則A的Feign 客戶端在解析的時候就會拋出FeignException異常(由於http code為500)。此時有兩種選擇,A捕獲到FeignException,本地事務中斷回滾,並告知客戶端服務整體不可用,不過這樣體驗不好。還有一種選擇,就是當A的feign客戶端配置了fallback,則發生FeignException時會進入對應的fallback代碼,那么在這段代碼里可以向消息隊列里發送這個調用請求!對應的B會監聽這個調用消息,然而此時無論B調用多少次都會報錯,不過沒關系。緊急發布一個補丁,修復導致ServerException的那個bug,然后部署上線。當新版本的B代碼監聽到這個消息時,請求就會處理成功,以達到最終一致性!
5、A調用B成功后,A本地事務提交出錯,這個問題比較惡心,粗略想了一下A try catch一下然后調用B的補償接口來使B的數據恢復正常。
最后,為了安全起見,微服務之前涉及到修改操作的接口,都要保證冪等性。最簡單的一個情況就是服務調用超時,Feign會自動重新發送請求幾次,那么假設某服務響應過慢,則極有可能導致請求重復提交,若沒有做到冪等則會產生錯誤數據。另外一個就是基於消息隊列來保證最終一致性,也有可能發生請求重復處理。