成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

如何做好“防御性編碼”?

原創 精選
開發 前端
在意識層面上,我們當然要摒棄“想當然”和“差不多”的思想,嚴肅評估這些問題發生的可能性,認真對待這些風險。但如若話題止步于此,那其實還是缺乏執行層面的指導意義的,激不起半點“漣漪”的。

作者 |  字白

一、防御性編碼的意義

類似于“防御性駕駛”對駕駛安全的重要性,防御性編碼目的概括起來就一條:將代碼質量問題消滅于萌芽。要做到“防御性編碼”,就要求我們充分認識到代碼質量的嚴肅性,也就是“一旦你覺得這個地方可能出問題,那基本它就會(在某個時刻)出問題”。當然,實際情況比這個更嚴峻。由于大家的編碼經驗和風格差異,導致大家的意識邊界是大小不一的,那些潛伏在意識邊界之外的“危險”更加隱蔽和不可琢磨。

在意識層面上,我們當然要摒棄“想當然”和“差不多”的思想,嚴肅評估這些問題發生的可能性,認真對待這些風險。但如若話題止步于此,那其實還是缺乏執行層面的指導意義的,激不起半點“漣漪”的。

這個文章目的也更多是關注到“實操層面”的引導。

二、如何防御性編碼?

以下需關注的具體方面更多來自于我的習慣和觀察,并且統一用偽代碼作問題示例。

歡迎大家把自己的“防御性編碼心得”在評論區分享出來。

1.并發沖突問題

這個問題在實際項目中,被錯誤地忽視的比例相當高。它的外在表現形式五花八門,但關鍵點是:“當你的代碼被并發調用時,它會怎么表現?”

我們心里要有個運行時的世界觀,代碼運行的Context是這樣的:多線程 -> 多進程 -> 多機器 -> 多集群。我們編碼時,要充分考慮代碼在上述世界觀多點并發的可能性,及相應的潛在后果。

舉幾個具體的問題例子):

  • 存在共享變量 或者 數據。(不限于堆內存,也可能是緩存、DB、文件等)

例子1:

有線程 A 和線程 B 兩個線程,需要更新「同一條」數據,會發生這樣的場景:

1).線程 A 更新數據庫(X = 1)

2) 線程 B 更新數據庫(X = 2)

3)線程 B 更新緩存(X = 2)

4) 線程 A 更新緩存(X = 1)

最終 X 的值在緩存中是 1,在數據庫中是 2,發生不一致。

例子2:


// 某個 Spring singleton Bean 'aService' 存在一個調用來源標記,記錄調用來源是HSF還是HTTP。
// 先 記錄來源標記。
aService.setSource(source);
// 再結合source執行其他邏輯。例如將上面記錄的source 和 其他參數 插入數據庫.
aService.doSomethings(params);

例子3 :

在一個系統中,有兩個價格類型 small 和 large,業務邏輯要求 small <= large,且 small 和 large 有2個入口可以分別修改。

目前方案是:對要改變的small或large,增加上面大小關系校驗,不通過則攔截,例如 改動small的入口上,校驗改后的small <= 系統里的large,不通過則不允許修改。

假如,最新需求要求:修改large的入口繼續攔截,但修改small的入口不再攔截,而是發現如果改后small > 系統的large,則將 系統large = 改后的small+0.1,讓 約束關系繼續成立。 這種改法有問題嗎?

答案:這種改法會有問題。即 small這個價格類型存有兩個鏈路同時修改,也是一種并發沖突問題。

舉個具體例子:

  • 初始時,系統的small = 2; large = 2;
  • 修改large 鏈路1:準備將 large 改為 3,檢查規則 3(改后large ) >= 2(系統small) 通過。準備寫入新的large (3)。
  • 修改small 鏈路2:準備降 small 改為 4, 發現 4(改后small)> 2(系統large) 不符合規則,則 準備 自動修改 large = 4(改后small)+ 0.1 = 4.1。準備寫入 改后small = 4,自動改后 large = 4.1;
  • 如果 鏈路2 最終先完成寫入,鏈路1再完成寫入。則 鏈路2寫入的 large=4.1 會被鏈路1 寫入的large=3 覆蓋。最終系統 large =3,而 系統small = 4;破壞了最初的small <= large 的約束。
  • 未考慮集群并發。

// 在短信發送服務中,控制對用戶的發送頻率
timestamp = rateLimitService.getMsgTimestamp(userId);
if( timestamp == null ){
rateLimitService.putMsgTimestamp(userId, now);
sendMsg(msg);
}else if( timestamp - now > 1 hour ){
rateLimitService.putMsgTimestamp(userId, now);
sendMsg(msg);
}
  • 非原子操作問題。
// 先查詢是否存在目標記錄
resultList = dbRepo.list(query);
// 有結果就更新,沒有就插入
if( resultList.size() > 0 ){
dbRepo.update(xxxx);
} else {
dbRepo.insert(xxxx);
}
  • 錯誤的發生并發

單個任務周期性的觸發,本來不會有并發問題。但因單次執行時間變長,導致先后兩次執行時間出現重疊。

2.事務問題

對于先A再B后C的這類組合操作,要仔細考慮保障一致性的必要性,做好是否做事務保障的評估。

事務即要求:對一組的operation combo,要保障好執行順序,保障好context的一致性,保障好結果的一致性。

  • 數據庫事務。 發生概率不高,大多會主動預防。

這個問題發生概率倒不高,也比較容易解決。但要注意,事務執行耗時不要太久,以及避免死鎖問題發生。

  • 上下文一致性問題。

以上傳并處理Excel文件為例,假如實現分為 2 步:

1) 前端調用后端API,上傳文件到Server的某個臨時目錄。

2)前端 在上傳完成時,調用后端另一個API,通知 后端處理此文件。

這個例子在集群環境中就會出現概率性成功或失敗的情況,集群節點數量越多,失敗概率越高。這是因為 前端的前后兩次請求調用到了不同節點上,執行上下文出現了不一致。

  • 順序一致性問題。

常見的,例如對于 ECS運行狀態的時序消息,如果下游消費者不是順序消費,而是并行消費,就可能導致最終記錄的狀態 與實際不符。

3.分布式鎖問題

分布式鎖日常也經常用到,在使用細節上存在一些容易忽略的盲點。

  • 獲取鎖

1)是阻塞式等待鎖,還是等不到鎖重試,還是等不到鎖直接返回。這個層面主要考量點,這個調用鏈路對時間和成功率要求是什么。例如,上游是用戶操作,那肯定不能阻塞在等鎖那里太久;

2)鎖的key設計很關鍵。合理設計lock key,能夠降低鎖碰撞的概率。例如,你的lock 是加在一個BU層面上,還是加到某個人身上,那沖突概率顯然差別很大。

3)對于 持久鎖,在循環執行業務邏輯時,要做好鎖的狀態檢查。

 RLock lock = redisson.getLock(lock);
lock.lock(-1L, TimeUnit.MINUTES);
// 獲取到鎖就持久占有,避免反復切換
while( !isStopped ){
if( lock.isHeldByCurrentThread() ){
// do some work
}else{
// try to acquire lock again.
}
SleepUtil.sleep(loopInterval, TimeUnit.MINUTES);
}

4)能用本地鎖 不用全局鎖。

  • 鎖超時

1)合理設置鎖的TTL,結合自己業務場景做取舍例如,加鎖之后執行大量數據的batch計算的場景。如果鎖TTL太長,那計算被異常中斷(如機器重啟)時,這個長TTL內是無法被其他節點/線程獲取到執行權限的;但如果TTL設置太短,那可能還沒等執行完成,鎖就被意外搶走了。

2)注意watchDog機制像Redisson之類的會有鎖的watchdog,超過設置或默認的時間,鎖就被偷偷釋放了。

  • 釋放鎖

1非必要情況下,避免強行釋放鎖,要檢查鎖的持有人是否是自己。

2對于沒有TTL的鎖,要考慮極端情況下(進程被強制殺死、機器重啟)的鎖狀態管理。否則意外一旦出現,鎖就永遠丟失了。

4.緩存問題

  • 緩存穿透問題

緩存和數據庫都沒有的數據,但被大量請求,導致DB壓力過大。常見的解決方式:對空值也進行緩存,但TTL設置相對較短。

  • 緩存擊穿問題

一般是緩存的熱點key發生過期失效,此時大量請求透過緩存 擊中DB,導致DB壓力過大。

常見解決方式:緩存查詢miss時,設置個互斥鎖,只允許一個request真實請求DB和重寫緩存,避免大量請求涌入。

  • 緩存雪崩問題

緩存中的大量數據在較短的時間段內集中過期。一般發生在流量一波波來,緩存創建時間和TTL很接近。

常見解決方案:在TTL設置上不是一刀切,而是在一個合理范圍內隨機浮動,避免緩存集中失效。

  • 緩存的一致性

一般情況下,一致性要求不會非常嚴格。但如果需要強一致性保障時,要考慮緩存和DB之間的數據強一致性。

一種可能的方案:只在寫DB時才寫緩存,讀DB操作不寫緩存。DB和緩存的寫操作要加鎖,避免并發問題。具體流程如下:

當寫DB請求發生時:

1刪除 緩存。此時讀操作緩存會miss,讀取到DB中的老值。

2寫入DB。此時讀操作緩存會miss,讀取到DB中的新值。

3寫入緩存。此時讀操作緩存會 hit,讀取到緩存中的新值(與DB新值一致)。

需要注意的是:

1緩存針對數據庫所有的數據記錄,可能導致緩存空間占用高,實際利用率卻不高。

2如果某個緩存key 是熱點,或者 流量比較大,盡管緩存“刪除-重寫入”間隔短,依然可能會引發 緩存擊穿問題。

3如果緩存寫入失敗,需要有相應的補償機制再寫入,且需關注 補償寫入與其他正常寫入的沖突和時序問題。

  • 緩存命中率

這個本身不是問題,但命中率低說明緩存的設計或使用存在問題,需要重新設計。

  • 熱點key問題

如果特定緩存節點CPU使用率遠高于其他節點,說明可能存在熱點key。這個時候需要合理對緩存key做拆分,將流量進一步打散。

5.失敗處理問題

這類問題雖屬于低級問題,但往往比較隱蔽。在異常發生時,選擇相應處理action時,我們要頭腦非常清醒。

  • 失敗處理

可能的處理方式:

1failover。失敗立即重試。

2failback。記錄失敗,后置處理。

3failfast。直接失敗,返回異常。

4ailsafe。忽略失敗,繼續流程。

這里不在于選擇那種處理方式,而是要“頭腦清醒”的結合自己場景需求做出選擇。

  • 注意默認值

一些情況下,我們會初始化時設定一些默認值、默認狀態等,對于這些情況要充分考慮異常發生時是否存在風險。

例如,在最開始時,代碼里配置了當時的開城信息,但這個狀態并沒有跟業務操作流程打通,也就是沒有辦法做到及時更新。

那隨著時間發展,開發了新的城市,那就可能產生問題。

6.switch配置問題

  • 分批推送的時間間隔

switch發布時,不同批次會有時間間隔,大部分場景下都可以容忍這個時間間隔。但個別情況下,可能引發諸如數據不一致等問題。

再使用switch時需要對這個問題做提前考慮,若不能容忍這種情況,那需要更換其他方案。

  • 內存值與持久值

switch的邏輯是這樣:

1switch會默認記錄代碼中的默認值。此時并不是 持久值。

2當在代碼中修改默認值時,switch平臺也會顯示代碼默認值。此時也并不是 持久值。

3只有在switch平臺修改值并推送成功,swith平臺會保存持久值。

4switch保存持久值之后,不管代碼修改默認值還是去掉 @AppSwitch 配置,持久值都是存在的。

如果你看到switch平臺上展示了開關值,以為已經持久化,然后在代碼里就把默認值刪掉,此時也可能導致故障。

  • 代碼重構注意事項

做代碼結構重構時,如果沒有指定switch的namespace,會導致你推送過的持久化開關失效,進而引發嚴重的線上故障。

關于應用級服務發現與接口級服務發現的區別和 dubbo 生態的解決方案,本文中不多贅述,可以參考劉軍前輩寫的文章文章《Dubbo 邁出云原生重要一步 應用級服務發現解析》

簡單來說,應用級服務發現需要開發者關心接口之外還要關心應用名,注冊中心的冗余信息較少;接口級服務發現開發者只需要引入接口名,但注冊中心的冗余信息較多。

  • 合理使用,避免濫用

switch 提供了簡單易用的配置化能力,但不要把應該正常編碼要考慮和處理的問題,丟到switch上做開關。否則,最后開關一大堆,維護越發困難,就隱藏了風險。

7.重大風險評估和處置

針對一個需求開發,我們需要評估風險及我們的承受能力。主要目的是 預防重大故障的發生,而不是要預防所有Bug。

關于風險處置,也沒有一個固定的標準。我建議是結合業務場景,評估風險概率和潛在問題的嚴重程度,最后來制定相應的解決方案。例如,如果發現有資損風險,那要采取一切手段把漏洞堵上;但如果只是小概率的漏掉釘釘通知,那增加相應的告警即可。

我們如何評估 重大風險呢?我建議分這么幾個環節做評估:

1梳理 關鍵的業務流。

2梳理 每個業務流的關鍵環節。

3梳理 每個關鍵環節的關鍵邏輯 和 關鍵上下游。

4結合自己場景,假定 關鍵邏輯 和 關鍵上下游 出現極端問題。例如 網絡掛掉、機器重啟、高并發來臨、緩存掛掉等。

這里需要強調一點,并非所有模塊都需要假定非常極端的情況,要結合自己實際業務要求、歷史風險等 來綜合判斷。

再舉個例子:

假設,有一個用戶資金轉賬系統,用戶可以通過App進行跨行轉賬操作。

那這個系統就要考慮到 轉賬超時、轉賬失敗等場景。同時還要考慮 轉賬超時 或 失敗時,是fail-fast 好,還是 fail-over好?

此外,還需要考慮到 App端的用戶交互設計,假如遭遇網絡中斷或超時,且用戶看不到任何問題提示,那用戶很可能再次發起轉賬嘗試,最后轉了兩筆的錢。

這個評估過程看上去有點冗長,但其實對于了解自己系統和需求細節的人來講,應該是很容易做到的。如果做不到那就只能加強細節的理解和學習了。

三、最后

以研發同學為中心,向內看:需持續提升防御性編碼的意識和實操能力;向外看:外部環境需要盡可能提供與之匹配的環境。

例如,在面臨有緊急DeadLine的需求時,防御性編碼的執行完整度就會受到一定影響。

責任編輯:武曉燕 來源: 阿里開發者
相關推薦

2023-09-27 22:52:52

2023-12-15 08:17:13

防御性編程代碼

2022-04-26 06:21:59

編程動態內存

2024-10-09 12:03:06

2014-09-01 10:46:57

2024-07-26 10:01:16

2022-03-11 07:47:56

防御性編程互聯網

2020-08-23 21:07:16

編程PythonJava

2022-05-13 12:14:44

CSS項目技能

2023-12-12 13:18:11

2023-12-12 09:27:07

編程碼農

2019-04-29 09:52:46

容器安全漏洞網絡安全

2020-07-22 07:00:00

微服務架構

2011-05-26 16:27:24

SEO

2023-03-18 20:51:16

Kali LinuxLinux

2021-01-19 09:59:02

招聘管理團隊

2022-06-22 08:02:01

業務監控Web站點監控

2013-07-10 09:22:59

云配置云實踐云應用程序接口

2011-04-18 13:20:40

單元測試軟件測試

2025-05-26 10:25:00

防御性編程開發編程
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 欧美黑人一区二区三区 | 99在线观看视频 | 国产999精品久久久久久 | 剑来高清在线观看 | 日韩av大片免费看 | 精品国产乱码一区二区三 | 99亚洲视频| 午夜视频一区二区 | 久久久久国产精品 | 欧美乱做爰xxxⅹ久久久 | 羞羞视频网页 | av中文在线 | 国产精品成人一区二区三区夜夜夜 | 久久久久国产精品午夜一区 | 国产欧美一区二区三区国产幕精品 | 91免费电影 | 亚洲国产欧美国产综合一区 | 久久久久黄色 | 欧美在线观看免费观看视频 | a成人| 国产精品高清一区二区三区 | 免费播放一级片 | 国产免费观看久久黄av片涩av | 亚洲成人一区二区 | 久久久久无码国产精品一区 | 视频在线一区二区 | 国产一区二区三区免费 | 日本亚洲一区 | 欧美一二精品 | 一本岛道一二三不卡区 | 亚洲激情在线观看 | 欧美九九九 | 四虎永久免费在线 | 亚洲精品9999久久久久 | 精品国产三级 | 欧美男人天堂 | 亚洲国产精品久久久久秋霞不卡 | av中文字幕在线观看 | 综合自拍| 久久久高清 | 国产不卡一 |