原創 锂昂 淘系技術 5月25日

在計算機科學中,魯棒性(英語: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)
正例:"test".equals(object)
反例:object.equals("test")
循環體内,字元串的聯接方式,使用StringBuilder的append方法進行擴充。
「說明」若直接用兩字元串拼接,反編譯出的位元組碼檔案顯示每次循環都會new出一個StringBuilder對象,然後進行append操作,最後通過toString方法傳回String對象,造成記憶體資源浪費。
String str = "start";
for (int i = 0; i < 100; i++) {
str = str + "hello";
}
代碼魯棒性是運用在程式設計過程中的,是過程導向結果産出的特性,是以并不能用一個典型案例覆寫全部。但結合上文氣泡任務需求的設計,我們可以針對特定細節詳細表述。
當使用者進入互動玩法頻道後,代碼邏輯是先擷取所有目前氣泡任務清單,然後判斷其狀态,最後根據氣泡優先級進行過濾展示。其中氣泡過濾過程采用了責任鍊模式。流程圖如下所示:
核心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的異常處理。
總結
魯棒性,是一種具有自我保護的系統特性,落實到細節的地方絕不止設計和開發環節。此外,上述設計和代碼建議,意圖不在于消除代碼的創新性,也不是以一種标準化的姿态限定代碼魔幻的邊界,而更多的是給出一種較好的方式處理做事。
系統魯棒性的建構絕不是一朝一夕就能搞定的,保持匠心精神、積累經驗、不斷學習才是其根本。如何做到系統穩如泰山,也許是每一位開發同學共同的使命之一吧。