<xmp id="63nn9"><video id="63nn9"></video></xmp>

<xmp id="63nn9"></xmp>

<wbr id="63nn9"><ins id="63nn9"></ins></wbr>

<wbr id="63nn9"></wbr><video id="63nn9"><ins id="63nn9"><table id="63nn9"></table></ins></video>

如何保證用戶重試操作的冪等性

服務不穩定是一類常態,面對此類場景恰當的應對策略應該是什么?退一步說,即使我們能夠確保第一方服務的穩定性,我們又應該如何面對網絡延遲以及掌控以外的不確定性?這都是本篇文章會談到的內容

本文是團隊內部分享的文字版,敏感信息已經抹去或者重寫。我們通過三個實際的線上問題來看看在今后的開發過程中可以如何避免此類問題

校驗是可選還是必選

用例1:學生可以在網站選擇指定的日期和時間預約老師進行會議,老師也需要設定在某一時間段內可以并行服務學生的數量,畢竟她的帶寬有限。但線上出現了老師在同一時間內被多個學生預約成功的情況,即預約數超出了她可以提供服務的上限。

用例2:在用戶第一次訪問網站前,他需要簽署一系列協議。但我們發現有些協議被連續簽署了多次,導致后續的功能出現了異常。在重現問題的過程中我們得知,確實可以通過復制瀏覽器標簽的方式來重復簽署同一份協議

這兩個問題的修復方式是顯而易見的:給后端有關接口添加校驗。但問題是,它們是否可以算作開發功能的失誤?用“九轉大腸”問句就是:是故意的還是不小心的?

經典的風險應對模型告訴我們,根據風險的危害和發生概率,我們可以使用四種策略來處理問題:avoid、reduce、retain、transfer

在我看來模型傳達給我們的不止于此;

  • 對于 retain,我認為它更想表達的不僅僅接納(什么都不做),而是盡可能用低成本的方式去做;
  • 對于 avoid,你可能無法完美 avoid,但也許你可以把風險往其他象限轉移,畢竟降低風險也是一種策略

回到這段開頭的兩個 case 上,我認為在功能設計之初,考慮到有限的使用頻率和可承受的風險,以及無從考證的交付壓力,不去接口校驗沒有問題。(我們一直以來缺乏對于數據增長的監控,很多問題的產生,尤其是性能問題都是在稍不留神間達到了代碼能夠支撐的閾值,這個問題之后再談)。但我們真就可以什么都不用做了嗎?至少我們可以讓代碼變得靈活一些:不需要去預測未來發生什么,讓代碼可能應對未來的變化即可:

于是,我們傾向于將演進能力構建到軟件中,如果項目可以輕松應對變化,那么架構師就不再需要水晶球 ——《演進式架構》人民郵電出版社

關鍵在于,你并不需要去預測什么會變化,你需要知道的是,變化必然會發生。程序應該保證盡可能的靈活性,這樣,不管未來發生什么變化,都可以應付得了——《簡約之美:軟件設計之道》人民郵電出版社

更復雜的問題

如果說前兩個用例的癥結和方案都清晰可見的話,下面這個用例也許可以帶來一些思考。

假設我們需要在頁面上展示申請處理進展,進展由步驟(step)構成。步驟的類型分為主步驟(step)和子步驟(sub step),可以混合使用進行串聯,如下圖所示

顧名思義,進展允許前進也就允許回滾。兩類步驟分別有屬于自己的回滾接口:

  • step 回滾:使用 PUT method 調用 /{progressID}/back
  • sub step 回滾:使用 PUT method 調用 /{progressID}/back,但是需要在 payload 里加上需要回滾的 sub step 所屬的 step ID

假設目前存在一個如下圖所示的步驟序列,當前的步驟位置處于尾聲

如果想要把這一系列步驟正確回滾,接口的調用順序如下:

但在排查一個問題時,我們發現用戶側的實際調用順序是這樣的:

這便導致當中的某個 sub step 被略過,數據沒有被正常清除

而為什么會出現這種情況?通過 Application Insights 我們發現,用戶在從點擊選擇發送回滾請求到服務器接收到請求,存在12秒的網絡延遲,實際代碼只花費了 276ms 來處理這個請求

而恰好 UI 又允許用戶在等待請求的返回過程中選擇重新取消等待界面,重新點擊發送

于是用戶在等待的過程中選擇不斷的重試

問題在哪

允許重試?

重試沒有罪,恰恰相反,重試是我們最重要的機制。服務不穩定是一個常態,重試可以幫助我們解決相當一部分問題。例如我在排查死鎖問題時,發現一旦死鎖給用戶帶來負面影響,用戶會選擇刷新頁面“自助”解決問題

甚至重試是應該根植在我們代碼中,無論前端還是后端,用于網絡請求的 client 應該對于首次失敗的請求默認進行重試,無需額外的代碼。

好的“基礎設施”(例如日志、鑒權、重試,以及這里的重試)代碼應該是毫無存在感的,很容易、甚至無意識的讓人做對很多事

關于重試策略,一篇來自 AWS 社區的文章非常值得我們參考《Timeouts, retries, and backoff with jitter》,重試時我們不僅需要加入 backoff(延遲) 和 jitter(波動) 參數,還需要考慮重試給服務器帶來的壓力等情況

接口不夠冪等?

不同的 HTTP method 是自帶冪等屬性的,例如 GET 天然冪等,而 POST 天然就是不冪等的。對于采用 PUT method 的 back 接口而言,也許冪等性沒有做好。但是冪等性不是所有問題的擋箭牌。

想象這么一個場景:假如我們有一個用于上傳特殊文件的 POST 接口 A,和只有在文件上傳成功之后才能工作的功能 B。如果 B 工作時只能允許有一份上傳成功的文件存在,而這個時候又是因為網絡原因導致用戶選擇上傳兩遍,那么出錯的是誰?

  • 用戶?用戶遲遲得不到反饋于是選擇重新上傳我不認為有什么錯
  • 接口?上傳文件用的 POST 接口天生不就是不冪等的嗎?

除此之外冪等性也是需要代價的,在我看來一個冪等接口的完美實現可以參考這篇同樣是來自 AWS 的文章《Making retries safe with idempotent APIs》,他們在請求中加入了 unique client request identifier 作為
標識符,用于后續服務判斷是否已經處理過相同的請求。

上面覆蓋的只是其中一類場景,實際的業務場景可能更復雜,例如要應對資源競爭的情況,如果想要了解更多接口的冪等實現,可以參考這篇文章《How to ensure idempotence》

用戶行為的冪等性

如何解決此類問題,尤其是在我們解決做解決方案的時候,需要注意保證用戶行為(或者說業務操作)的冪等性,而不是僅僅關注接口本身,因為一個操作通常是由多個請求,甚至前后端的配合同時完成的,例如一個 step 可不可以被回滾多次?假如一個回滾操作需要調用多個接口,部分成功會不會有任何的風險?

如何實現此類冪等性,我的建議是從以下這幾個維度考慮:

  • 什么都不做優于去做些什么:我們是不是真的需要去保證冪等性?考慮到風險、概率、交付壓力,什么都不做也是可以接受的

  • 預防問題優于事后補救:優先考慮從輸入側解決問題,比如從前端 UI 上控制,或者接口入口處進行校驗。因為待問題出現之后再考慮修復數據的代價通常是不可控的,快速失敗很重要。

  • 低成本優于高成本:如果真的要做冪等性校驗,我們是不是要做端到端的整套功能?大可不必。如果風險不大,我們可以只在日志中拋出錯誤而不進行 UI 提示。某些校驗甚至可以通過建立數據庫約束來解決

  • 轉移成本:GIGO (Garbage in, garbage out) 原則,不要嘗試去猜測并且修復用戶數據。校驗失敗之后我們可以把數據的修復工作交還給用戶。舉個不恰當的例子,假如某個后續功能需要與一個身份證件相關聯,代碼如果發現了多個身份證件,我們應該拋出的問題是:“我們發現了多個多個身份證件,請刪除額外的多個身份證件 再重試”,而不是“我們發現了 4 個多個身份證件,請問你需要選用哪一個?”


你可能也會喜歡:

posted @ 2023-05-24 22:42  hh54188  閱讀(365)  評論(1編輯  收藏  舉報
人碰人摸人爱免费视频播放

<xmp id="63nn9"><video id="63nn9"></video></xmp>

<xmp id="63nn9"></xmp>

<wbr id="63nn9"><ins id="63nn9"></ins></wbr>

<wbr id="63nn9"></wbr><video id="63nn9"><ins id="63nn9"><table id="63nn9"></table></ins></video>