關於魯棒性的思考
在計算機科學中,魯棒性(英語:Robustness)是指一個計算機系統在執行過程中處理錯誤,以及算法在遭遇輸入、運算等異常時繼續正常運行的能力。
魯棒性關注的重點在於系統的穩定性,在不同場景下衍生了複雜的設計考量,且本身是一個廣泛且難以具像化的特性。因此,針對特定目標實現魯棒性分析,形成切實可行的魯棒性意識,保障安全性。
基於魯棒性分析,以設計規約爲目標,有三個維度可以拆解:輸入、處理、輸出;以代碼規範爲核心,我們可以從三個方面來分析,分別爲:代碼質量、代碼性能以及代碼優雅。
設計規約
▐ 失敗設計思維
針對輸入和處理環節,失敗設計思維是保證魯棒性的有效設想。該思維要貫穿代碼生命週期始終,把失敗當作代碼設計中合理存在,提前準備好從運行失敗的場景中恢復。倡導防禦式編程思想,拒絕契約式編程。
-
入參判空、有效性檢驗。
-
系統設計時識別弱依賴,並針對性地設計降級、限流等應急預案,保證核心邏輯正常可用。
-
在考慮主幹功能的同時,要充分考慮評估異常流程與業務邊界。
-
......
▐ 圖式表達設計
針對處理環節,圖式表達設計保證魯棒性的有效舉措。在複雜多變的業務場景中,圖式表達往往能夠以清晰、結構化的展現業務關聯關係,對技術鏈路包括失敗異常分支也有充分的分析幫助。
-
如果某個業務對象狀態超過 3 個,使用狀態圖來表達並且明確狀態變化的觸發條件;狀態圖的核心是對象狀態,首先明確對象有多少種狀態,然後明確狀態間是否存在直接轉換關係,再明確觸發狀態轉換的條件是什麼,最終輸出狀態轉移圖。注:狀態圖中的狀態在代碼中必須集中定義。
-
如果系統中某個功能的調用鏈路上涉及對象超過 3 個,使用時序圖來表達並且明確調用環節的輸入與輸出。時序圖反映了一些列對象間的交互和協作關係,可以清晰立體地反映系統間調用縱深鏈路。
-
如果系統中模型類超過 5 個,並且存在複雜的依賴關係,使用類圖來表達並且明確類之間的關係。
-
如果系統中超過 2 個對象之間存在協作關係,並且需要表示複雜的處理流程,使用活動圖來表示。
-
......
▐ 異常錯誤處理
針對輸出環節,異常錯誤處理是保障魯棒性的重要依據。業務代碼必然會有錯誤失敗出現,是否符合預期表現,是否在正常處理流中,是否可以快速對錯誤定位,往往要有一定的判斷依據。面對異常分支,就需要異常錯誤輸出,也是系統監控的基礎。
-
錯誤碼設計。錯誤碼能夠快速知曉錯誤來源,同時也能給予依賴者的確定性表達,提高魯棒性。
-
異常日誌輸出。控制異常日誌輸出級別,error 級別只記錄系統邏輯出錯、異常或者其他重要的錯誤信息。
-
......
▐ 實戰 Case
-
需求背景
聚划算章魚互動升級爲 “聚財氣” 頻道,新增氣泡獎勵玩法。氣泡獎勵分登錄獎勵和時長獎勵,其中時長獎勵包括獎勵 1 倒計時 30 秒、獎勵 2 每日 9 點以及獎勵 3 每日 20 點。
場景演示:用戶在 10:00 進入頻道後,收取完登陸獎勵,喚起了一個 30 秒後的獎勵的氣泡;30 秒後用戶點擊領獎,喚起了一個提示今日 20:00 可領的提示(該獎勵未領);用戶次日再來,收取完登陸獎勵後喚起了 30 秒後的獎勵氣泡....
實現效果
-
技術設計
通過氣泡任務的需求描述,簡單分析可以得知,任務開始到權益發放間有狀態變更,氣泡任務間有優先級邏輯。因此,基於設計規約,我們可以對需求進行清晰的分析和開發設計。
1、圖式表達設計
氣泡任務的複雜度主要在於多狀態的變更,所以採用圖式表達方式完成狀態的變遷。可以看出,運用狀態圖是較合適的。(狀態圖:主要用於描述一個對象在其生存期間的動態行爲,表現爲一個對象所經歷的狀態序列,引起狀態轉移的事件,以及因狀態轉移而伴隨的動作)
氣泡任務狀態圖
氣泡任務間展示狀態圖
2、失敗設計思維
針對氣泡任務,失敗設計思維的側重在於防禦式編程和服務降級限流。在防禦式編程中,利用斷言型接口,對氣泡透傳前置條件校驗、狀態扭轉識別以及有效性檢驗。同時,在服務降級預案中,考慮到氣泡任務並不影響玩法頻道的用戶主流程,因此設計了兩種預案:一是獎勵資格和權益發放大面積失敗或異常時,氣泡任務全部降級處理;二是特定氣泡邏輯存在異常問題時,該氣泡降級關閉。此外,設定服務限流閾值,在大促流量高峯時保護系統穩定。
3、異常錯誤處理
異常錯誤處理主要在於失敗後的反應動作和前臺用戶表達。氣泡任務狀態轉移中,會存在獎勵資格和權益發放失敗的現象。失敗的發生有着難以枚舉的原因。針對失敗,首先保持冪等性,進行系統重試或者用戶行爲重試;其次,失敗異常日誌輸出,利用錯誤碼設計儘可能準確描述失敗原因;最後,異常和錯誤監控,基於分鐘級錯誤日誌統計報警,開發同學可第一時間介入定位問題。另外重要的一點是,由於真正使用的是用戶,所以前臺表達一定要是友好的、便於理解的,不然歧義的表述會造成大面積輿情發生。
-
小結
基於上述三點,貫穿氣泡任務的設計、開發等過程,不同維度地保證了系統魯棒性。此外,在實際開發階段,氣泡任務採用了責任鏈模式來實現的,可動態調整氣泡間依賴關係,提供一定的擴展性。
代碼魯棒性
以具體場景和實例來描述代碼規範和技巧,提升代碼魯棒性和系統穩定性。
▐ 代碼質量
-
集合處理
在使用 java.util.stream.Collectors 類的 toMap() 方法轉爲 Map 集合時,一定要使用含有參數類型爲 BinaryOperator,參數名爲 mergeFunction 的方法,否則當出現相同 key 值時會拋出 IllegalStateException 異常。
「說明」參數 mergeFunction 的作用是當出現 key 重複時,自定義對 value 的處理策略。
List<Pair<String, Double>> pairArrayList = new ArrayList<>(3);
pairArrayList.add(new Pair<>("version", 6.19));
pairArrayList.add(new Pair<>("version", 10.24));
pairArrayList.add(new Pair<>("version", 13.14));
Map<String, Double> map = pairArrayList.stream().collect(
// 生成的map集合中只有一個鍵值對:{version=13.14}
Collectors.toMap(Pair::getKey, Pair::getValue, (v1, v2) -> v2));
String[] departments = new String[] {"iERP", "iERP", "EIBU"};
// 拋出IllegalStateException異常
Map<Integer, String> map = Arrays.stream(departments)
.collect(Collectors.toMap(String::hashCode, str -> str));
在使用 java.util.stream.Collectors 類的 toMap() 方法轉爲 Map 集合時,一定要注意當 value 爲 null 時會拋 NPE 異常。
「說明」在 java.util.HashMap 的 merge 方法裏會進行如下的判斷
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}
List<Pair<String, Double>> pairArrayList = new ArrayList<>(2);
pairArrayList.add(new Pair<>("version1", 4.22));
pairArrayList.add(new Pair<>("version2", null));
Map<String, Double> map = pairArrayList.stream().collect(
// 拋出NullPointerException異常
Collectors.toMap(Pair::getKey, Pair::getValue, (v1, v2) -> v2));
Collections 類返回的對象,如:emptyList()/singletonList() 等都是 immutable list,不可對其進行添加或者刪除元素的操作。
ArrayList 的 subList 結果不可強轉成 ArrayList,否則會拋出 ClassCastException 異常:在 subList 場景中,高度注意對父集合元素的增加或刪除,均會導致子列表的遍歷、增加、刪除產生 ConcurrentModificationException 異常。
「說明」subList() 返回的是 ArrayList 的內部類 SubList,並不是 ArrayList 本身,而是 ArrayList 的一個視圖,對於 SubList 的 所有操作最終會反映到原列表上。列表改動均會引起 checkForComodification 異常
private void checkForComodification() {
if (this.modCount != l.modCount)
throw new ConcurrentModificationException();
}
在使用 Collection 接口任何實現類的 addAll() 方法時,都要對輸入的集合參數進行 NPE 判斷。
「說明」在 ArrayList#addAll 方法的第一行代碼即 Object[] a = c.toArray();其中 c 爲輸入集合參數,如果爲 null,則直接拋出異常。
泛型通配符 <? extends T> 允許調用讀方法 T get() 獲取 T 的引用,但不允許調用寫方法 set(T)傳入 T 的引用(傳入 null 除外);<? super T > 允許調用寫方法 set(T)傳入 T 的引用,但不允許調用讀方法 T get()獲取 T 的引用(獲取 Object 除外)。
「說明」PECS (Producer Extends Consumer Super) 原則:如果需要返回 T,它是生產者(Producer),要使用 extends 通配符;如果需要寫入 T,它是消費者(Consumer),要使用 super 通配符。因此,頻繁往外讀取內容的,適合用 <? extends T>。經常往裏插入的,適合用 <? super T>。
不要在 foreach 循環裏進行元素的 remove/add 操作。remove 元素請使用 Iterator 方式,如果併發操作,需要對 Iterator 迭代器對象加鎖。
List<String> list = new ArrayList<>();
list.add("targetItem");
list.add("other");
for (String item : list) {
if ("targetItem".equals(item)) {
list.remove(item);
}
}
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (刪除元素的條件) {
iterator.remove();
}
}
-
計算處理
禁止使用構造方法 BigDecimal(double) 的方式把 double 值轉化爲 BigDecimal 對象。
「說明」BigDecimal(double) 存在精度損失風險,在精確計算或值比較的場景中可能會導致業務邏輯異常。如:BigDecimal g = new BigDecimal(0.1f); 實際的存儲值爲:0.100000001490116119384765625
優先推薦入參爲 String 的構造方法,或使用 BigDecimal 的 valueOf 方法,此方法內部其實執行了 Double 的 toString,而 Double 的 toString 按 double 的實際能表達的精度對尾數進行了截斷。
BigDecimal recommend1 = new BigDecimal("0.1");
BigDecimal recommend2 = BigDecimal.valueOf(0.1);
-
日期處理
獲取當前毫秒數:System.currentTimeMillis(); 而不是 new Date().getTime()
「說明」如果想獲取更加精確的納秒級時間值,使用 System.nanoTime 的方式。在 JDK8 中,針對統計時間等場景,推薦使用 Instant 類。
日期格式化時,傳入 pattern 中表示年份統一使用小寫的 y。
「說明」日期格式化時,yyyy 表示當天所在的年,而大寫的 YYYY 代表是 week in which year,意思是 當天所在的周屬於的年份,一週從週日開始,週六結束,只要本週跨年,返回的 YYYY 就是下一年。
表示日期和時間的格式如下所示
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
▐ 代碼性能
判斷所有集合內部的元素是否爲空,使用 isEmpty() 方法,而不是 size()==0 的方式。
「說明」任何 Collection.isEmpty() 實現的時間複雜度都是 O(1),但是某些 Collection.size() 實現的時間複雜度可能是 O(n) 。
如 ConcurrentLinkedQueue 的 size() 是將所有元素重新統計了一遍,因此時間複雜度爲 O(n)。
Map<String, Object> map = new HashMap<>(16);
if(map.isEmpty()) {
System.out.println("no element in this map.");
}
集合初始化時,指定集合初始值大小。
「說明」HashMap 使用如下構造方法進行初始化,如果暫時無法確定集合大小,那麼指定默認值(16)即可;如果 hashMap 存放元素較多,由於沒有設置容量初始大小,隨着元素增加而被迫不斷擴容,resize() 方法不斷調用,反覆重建哈希表和數據遷移。當放置的集合元素個數達千萬級時會影響程序性能。
/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
利用 Set 元素唯一的特性,可以快速對另一個集合進行去重操作,避免使用 List 的 contains() 進行遍歷去重或者判斷包含操作
▐ 代碼優雅
外部正在調用或者二方庫依賴的接口,不允許修改方法簽名,避免對接口調用方產生影響。接口過時必須加 @Deprecated 註解,並清晰地說明採用的新接口或者新服務是什麼。
Object 的 equals 方法容易拋空指針異常,應使用常量或確定有值的對象來調用 equals。
「說明」推薦使用 JDK7 引入的工具類 java.util.Objects#equals(Object a, Object b)
循環體內,字符串的聯接方式,使用 StringBuilder 的 append 方法進行擴展。
「說明」若直接用兩字符串拼接,反編譯出的字節碼文件顯示每次循環都會 new 出一個 StringBuilder 對象,然後進行 append 操作,最後通過 toString 方法返回 String 對象,造成內存資源浪費。
String str = "start";
for (int i = 0; i < 100; i++) {
str = str + "hello";
}
▐ 實戰 Case
代碼魯棒性是運用在編程過程中的,是過程導向結果產出的特性,所以並不能用一個典型案例覆蓋全部。但結合上文氣泡任務需求的設計,我們可以針對特定細節詳細表述。
當用戶進入互動玩法頻道後,代碼邏輯是先獲取所有當前氣泡任務列表,然後判斷其狀態,最後根據氣泡優先級進行過濾展示。其中氣泡過濾過程採用了責任鏈模式。流程圖如下所示:
核心 Filter
/**
* 過濾器抽象
*
* @author la.lda
* @date 4/12/21
*/
@Data
@Slf4j
public abstract class Filter {
/**
* 氣泡類型
*/
public BubbleType bubbleType;
/**
* 上一氣泡過濾器
*/
public Filter nextFilter;
/**
* 下一氣泡過濾器
*/
public Filter beforeFilter;
public Boolean beforeFilter(BubbleContext bubbleContext) {
return true;
}
public void afterFilter(BubbleContext bubbleContext) {
}
/**
* 氣泡過濾邏輯
*
* @param bubbleContext
*/
abstract void Filter(BubbleContext bubbleContext);
/**
* 鏈式過濾器核心邏輯
*
* @param bubbleContext
*/
void doFilter(BubbleContext bubbleContext) {
if (bubbleType == null || bubbleContext == null || !bubbleContext.bubbleContextEffective()) {
return;
}
if (!beforeFilter(bubbleContext)) {
return;
}
Filter(bubbleContext);
afterFilter(bubbleContext);
if (nextFilter != null) {
nextFilter.doFilter(bubbleContext);
}
}
}
在 doFilter 核心邏輯中,多處進行了判空和有效性檢查,是防禦式編程的典型行爲。此處沒有用到 try catch 捕獲異常,其考慮是爲了將異常傳導到業務層,利於定位問題,因此在業務調用處存在 try catch 的異常處理。
總結
魯棒性,是一種具有自我保護的系統特性,落實到細節的地方絕不止設計和開發環節。此外,上述設計和代碼建議,意圖不在於消除代碼的創新性,也不是以一種標準化的姿態限定代碼魔幻的邊界,而更多的是給出一種較好的方式處理做事。
系統魯棒性的構建絕不是一朝一夕就能搞定的,保持匠心精神、積累經驗、不斷學習纔是其根本。如何做到系統穩如泰山,也許是每一位開發同學共同的使命之一吧。
作者 | 鋰昂
編輯 | 橙子君
出品 | 阿里巴巴新零售淘系技術
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/gvZ009wPwkUmOAwy707Uaw