首頁  >  文章  >  Java  >  Exception與Result的介紹(程式碼範例)

Exception與Result的介紹(程式碼範例)

不言
不言轉載
2019-03-12 15:52:004033瀏覽

這篇文章帶給大家的內容是關於Exception與Result的介紹(程式碼範例),有一定的參考價值,有需要的朋友可以參考一下,希望對你有幫助。

在分散式系統開發中,我們經常需要將各種各樣的狀態碼、錯誤訊息傳遞給最外層的呼叫方,這個呼叫方通常是http/api接口,錯誤訊息例如登入失效、參數錯誤等等。

最外層介面暴露的資料通常是類似{code, msg, data}這樣的json格式,這一點沒有任何爭議。

但是分散式系統的節點之間RPC呼叫、節點內部方法呼叫中,通常會用ServiceException或Result的方式進行錯誤訊息的傳遞,這兩種方式有什麼差別以及孰優孰劣呢?本文著重於開發效率與系統效能探討這個問題。

Result介紹

這是一種比較常見的錯誤訊息傳遞方式,某些大廠甚至直接將它們設為技術規範,強制各個團隊採用這種方式。常見的Result模板如下:

@Data
public class Result<T> {
    private int code; // 也可以是String等
    private String msg;
    private T data;
}

在系統開發中的應用通常是這樣的:

Result<UserModel> userModelResult = userService.query(userId);
if (!userModelResult.isSuccess() || userModelResult.getData != null) {
    return Result.fail(userModelResult); // 透传错误
}
UserModel userModel = userModelResult.getData();
if (userModel.getStatus() != UserStatusEnum.NORMAL) {
    return Result.fail("user unavaliable"); // 用户不可用
}
// ... 正常使用UserModel

在比較複雜的分散式微服務環境中,類似的程式碼非常之多,每個依賴服務的呼叫都伴隨著一段類似的容錯邏輯。

這種模式比較類似Golang語言中的錯誤碼處理,這也是Golang比較被人詬病的地方,也就是每一步都得進行錯誤判斷。

更殘酷的現實是,儘管有了Result封裝,但仍然會有後端系統的Exception透傳過來。在我接觸過的實際應用中,這種突破Result封裝的異常透傳絕非個例,我自己負責的系統在調用更後端的國內最強交易系統時,就曾接到過最內部交易中心TC的業務異常,追蹤問題時追蹤的團隊就有不只5個。

ServiceException介紹

顧名思義,這個方式就是使用異常中斷將正常邏輯與例外邏輯分割。

在系統開發中,大部分錯誤都需要直接中斷服務,直接將錯誤回饋給用戶,正因為如此,我們在使用Result時,經常需要寫類似if(result.isFail( )){return…}這樣的程式碼。而使用ServiceException,我們就可以省略掉絕大部分類似的程式碼。

通常ServiceException可以這樣定義:

@Getter
public class ServiceException extends RuntimeException {
    private final int code;
    private final String msg;
    public ApiException() {
        this(-1, null);
    }
    public ApiException(Code code) {
        this(code, null);
    }
    public ApiException(Code code, String msg) {
        super(msg);
        this.code = code;
        this.msg = msg;
    }
}

系統內部元件在遇到資料缺失、越權存取、登入失效、帳戶鎖定等異常情況時,直接拋出ServiceException中斷邏輯,然後由最外層的Filter或Aspect捕捉異常,提取其中的code和msg會回傳給使用者。

實際使用的程式碼邏輯類似這樣:

UserModel userModel = userService.query(userId); // userID不存在、不可用等隐藏在异常中
// ... 使用userModel

這種方式明顯優雅、精簡了許多,對於開發效率的提高以及後期維護都有幫助。

但是在坊間有許多流言聲稱,使用異常中斷會影響性能,甚至有人通過簡單的性能測試推出異常中斷的性能耗時比返回Result快幾百倍雲雲。

效能測試

針對效能問題,我也進行了一個簡單的測試,具體測試程式碼請參考:

https://github. com/sisyphsu/b...

這裡使用JMH進行效能測試,說到benchmark,真的是羨慕golang語言自帶的test庫,實在是太方便了。

測試內部的業務邏輯非常簡單,只是呼叫一次System.currentTimeMillis()並傳回long時間戳記。

效能測試中分別使用Result傳回值以及拋出Exception,針對拋出例外的效能測試,又增加的不同深度的呼叫堆疊測試,這是因為Java在拋出例外時,需要分析目前Thread的棧,而呼叫棧越深,所造成的效能損耗就越大。具體棧深度取值為1、10、100:

Test.test                  avgt    5  0.027 ± 0.001  us/op
Test.testException         avgt    5  1.060 ± 0.045  us/op
Test.testDeep10Exception   avgt    5  1.826 ± 0.122  us/op
Test.testDeep100Exception  avgt    5  9.802 ± 0.411  us/op

乍一看,異常堆疊深度為100的效能損耗確實是普通方法呼叫的360倍,有的人也確實是基於這個理由得出Java異常中斷效能損耗嚴重的結論。

分析效能的影響

但是要注意時間單位,只是微秒而已,毫秒的千分之一、秒的百萬分之一。

假設某個微服務單CPU吞吐量為1000QPS,而其中有10%是非法請求,那麼異常中斷的效能損耗也只是萬分之一而已,對於服務耗時的影響也只是0.001毫秒而已。

在效能測試中,業務耗時只是取得系統時間,大概耗時為25ns。正因為如此才顯得異常中斷的性能損耗達到恐怖的“幾百倍”,但是如果業務耗時從25ns變為25us、25ms呢?

再談效能瓶頸

我們在分析系統效能時,一定要搞清楚它的數量級以及效能瓶頸,切記陷入效能最佳化的困境。

舉個粗例子,在一般服務中,利用了索引的DB操作在1~10毫秒之間,存取分散式Cache的耗時在3~30毫秒之間,微服務RPC的網路效能損耗在3~10毫秒之間,客戶端與伺服器之間的網路耗時在5~300毫秒之間,如此之類等等。在這種情況下,優化0.001毫秒的性能隱患無異於撿了芝麻丟了西瓜。

我曾經寫過類似TCP的底層網路協議,在那種高頻場景中,演算法優化帶來0.1微秒的效能最佳化就意味著每秒鐘吞吐量幾成甚至幾倍的提升,但是在分散式呼叫的低頻場景中,這種效能用處沒有任何用處。

另外一個例子,幾年前我和同事在討論DB資料表設計時,因為訂單狀態使用什麼長度的int而爭執的面紅脖子粗,現在想想,訂單狀態上優化的1個字節,長年累月下來也只是節省不到1MB的磁碟空間而已,有什麼用呢?

RPC中的異常中斷

對於使用Dubbo、HSF這種遠端呼叫框架而言,使用異常中斷進行錯誤訊息傳遞,需要注意一點就是,異常類型需要設計為通用的,即各微服務都引用的基礎型別。

在某廠的技術規格中有說到:

1) 使用拋異常回傳方式,呼叫方如果沒有捕獲到就會產生運行時錯誤。

2) 如果不加棧訊息,只是new自訂異常,加入自己的理解的error message,對於呼叫端解決問題的幫助不會太多。如果加了堆疊訊息,在頻繁呼叫出錯的情況下,資料序列化和傳輸的效能損耗也是問題。

我對這種技術規範相當的不以為然。

首先業務異常本來就需要呼叫方透傳給最外層,諸如資料不存在、登入失效、使用者鎖定這種異常,中間的呼叫方捕獲了也往往沒有什麼用。

其次是鬼扯效能損耗,這種低頻的資料序列化和內網傳輸會有什麼樣的效能損耗呢?棧訊息透傳給呼叫方也有益於故障排查,我曾經接到過TC的異常棧信息,根據棧中的package直接就繞過三四層找到了底層出錯的地方,可以說是節省了大量的時間。

結論

在分散式微服務中,採用異常中斷可以大幅精簡業務程式碼,對於效能的影響也微乎其微。

輔助以@NotNull、@Nullable等註解,可以讓分散式開發如風一般的快速便捷。在複雜的服務網路中,業務異常也可以方便開發人員精確地定位錯誤,避免大家順著呼叫鏈一層一層追蹤故障點的尷尬情境。

以上是Exception與Result的介紹(程式碼範例)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:segmentfault.com。如有侵權,請聯絡admin@php.cn刪除