基於冪等表思想的冪等實踐

一、爲什麼需要冪等

  • 分佈式場景下,多個業務系統間實現強一致的協議是極其困難的。一個最簡單和可實現的假設就是保證最終一致性,這要求服務端在處理一個重複的請求時需要給出相同的迴應,同時不會對持久化數據產生副作用(即多次操作與單次操作的結果需要是業務角度一致的)。

  • 一個API擁有冪等能力的話,調用發起方就可以很安全的進行重試。這符合我們普遍的假設。提供冪等能力是服務提供方必須需要做的事。

  • 擁有冪等能力的話可以保證我們的接口不會被各種異常重試或惡意請求鎖衝擊。

二、冪等方式

不同的場景下(常見的是【界面和後端接口交互場景】和【接口於接口交互場景】),冪等方式有很多並且各不相同,都各有一些侷限性和缺點!!!

  • 可以基於【業務key + 業務狀態機 + 樂觀鎖】去做冪等實現(一般適用於比較簡單的update場景)

    • 比如:更新訂單狀態爲 finished 的場景中,先根據訂單號查詢訂單,判斷訂單狀態是否爲finished,若不是則更新爲finished
  • 可以基於【業務key + 分佈式鎖 + 業務狀態機】去做冪等實現

    • 比如:新增用戶信息場景中,執行方法前先加分佈式鎖(防併發),以用戶身份證號爲查詢條件,查詢用戶,如果用戶不存在則進行新增。如果用戶存在則冪等處理
    • 這種方案一般有個固定的流程:【一鎖、二判、三執行
  • 可以基於【業務key + 唯一索引】去做冪等實現(一般適用於新增數據場景)

    • 比如:新增用戶信息場景中,以用戶身份證號爲唯一鍵,建立唯一索引,新增用戶時通過捕獲唯一鍵衝突異常(DuplicateKeyException)進行冪等控制
  • 可以基於【Redis + token模式】去做冪等實現(多用於界面和接口交互,接口於接口交互不太適用,該方案也是比較常見的方案,但不在本次討論範圍中)

    • 比如:用戶提交填寫好的表單信息,多次重複提交時保證僅僅真正執行一次,其餘的都冪等返回相同結果
  • 可以基於【冪等表】去做冪等實現(比較通用的一種方案,具體效率取決於存儲冪等記錄的存儲介質)

    • 比如:消費MQ消息時,爲了避免消息重複消費,消費消息前可以先插入一條冪等記錄,然後再執行消費邏輯,消費完成後修改冪等記錄的冪等狀態爲消費成功!
    • 接口互調的情況類似

三、冪等設計原則

一個具有冪等性的服務,要求無論重複請求在多麼極端的情況下發生,都要表裏如一,此時必須滿足:

  • 對外:返回完全相同的結果

  • 對內:自身狀態不再發生任何改變

  • 對於服務提供方來說:嚴格來說需要請求中的字段完全一樣,服務提供方纔認爲是重複請求。但是在實際環境中我們可能沒有這麼嚴格的要求,我們一般認爲只要關鍵的業務參數相同,那麼他就屬於重複請求,應該被冪等處理。

  • 對於服務調用方來說:需要做好冪等結果處理,多次請求返回相同結果需要正確被處理

  • 冪等設計要儘量從簡單、可靠、高效(過多的冪等邏輯會對可用性和性能造成影響)角度出發

    • 簡單:冪等流程和邏輯要儘量簡單
    • 可靠:不僅僅在正常運行的情況下要保證冪等的可靠性,在某些異常場景下也要儘量保證冪等的可靠性,否則該冪等設計的意義將大打折扣
    • 高效:冪等邏輯執行不能高耗時,針對於一些高併發的接口需要做到儘量減少冪等邏輯執行耗時
  • 通用冪等組件設計易用性和可擴展性也同樣重要

四、常見冪等場景例子

  • MQ消息消費場景中】,由於MQ爲了保證消息投遞成功,可能會發起多次重試,那麼消費者方便需要保證重複的消息能夠被冪等處理(比如:監聽用戶支付成功消息進行生成支付單)
  • 界面和接口交互場景中】,前端重複提交數據,後臺接口需要保證只執行一次,其餘重複請求均冪等返回(比如:用戶重複提交訂單、重複提交錄入的用戶信息)
  • 接口互調的場景中】,調用方可能由於多種原因沒能收到響應結果而發起重試(比如:數據同步、庫存扣減等),此時被調用方需要保證重複調用冪等處理

五、冪等實踐

  • 爲了將冪等這個常見的通用需求儘量設計得通用化,我們這裏採用【冪等表 + 冪等狀態機】來實現,該方案可以適用於絕大部分的【界面 + 接口交互】和【接口 與 接口交互】模式
  • 如果項目僅僅是界面和接口交互模式,那麼採用【Redis + token】方案也是一個不錯的選擇
  • 當然,軟件工程中幾乎沒有銀彈,很難有一種完美適用與所有場景的方案

1、設計流程

  1. 調用放發起請求,請求到達服務提供方

  2. 獲取指定的業務key作爲唯一的冪等鍵,構建冪等記錄(此時冪等記錄status爲處理中(processing)),然後嘗試將冪等記錄寫入存儲介質(可以是Redis也可以是MySQL或其他存儲介質)

  3. 如果冪等記錄寫入成功,則執行業務邏輯

  4. 業務邏輯執行完畢,通過唯一鍵修改冪等記錄的status爲成功(success

  5. 如果冪等記錄寫入失敗,則說明冪等記錄已存在(該業務key對應的數據,之前有被執行過),需要進行如下處理:

  6. 通過冪等唯一鍵查詢冪等記錄,並且判定冪等記錄的status

    1. 如果status爲成功(success),則說明上次已經執行過該業務了,本次無需再重複執行,獲取上次執行的結果(如果有需要的話)冪等返回即可

    2. 如果status爲處理中(processing),則說明已經有其他線程正在處理業務數據 或者是 極端情況下應用宕機導致的異常情況。此時需要判定【請求處於處理中(processing)狀態的時長】,並且結合應用配置的【允許的最大業務執行時長】進行判斷

      • 處於processing狀態的時間已經超過配置的【允許的最大業務執行時長】,則嘗試以樂觀鎖的方式重新修改冪等記錄,如果修改成功則執行業務邏輯,反之則拋出併發異常。
      • 處於processing狀態的時間沒有超過配置的【允許的最大業務執行時長】,那麼直接拋出併發請求異常

2、問題思考

關於上述的冪等實現流程中,極端情況下,有如下幾點需要思考和注意的問題點

  • 極端情況下,如果插入冪等記錄成功,並且正常執行了業務流程,此時更新冪等狀態爲success時出現異常(比如存儲冪等記錄的存儲介質宕機了),此時是否需要處理該異常,還是說拋出異常中斷流程???如果拋出異常會有什麼影響?如果catch異常會有什麼影響?

    • 方案一、拋出異常:如果拋出異常中斷流程,那麼調用方應該感知到調用失敗了,但是實際上業務流程已經執行完畢,這種情況如果調用方發起重試,那麼冪等便會失效(同一個業務code被執行了兩次)
    • 方案二、不拋出異常:如果不拋出異常,接口會繼續執行,然後返回數據給調用方,如果調用方收到了返回數據,那麼便不會發起重試了,不會有冪等問題。但是此時冪等記錄的狀態仍然是處理中(processing),再指定了業務最大執行時間的情況下,如果調用方【超過指定的最大執行】時間再次發起重試,那麼冪等仍然失效(當然我們可以不指定業務最大執行時間)
    • 經過上述兩種情況的比較,我們一般傾向第二種方案,自己處理掉異常,並且做一層 【兜底策略】 (比如告警或記錄該條冪等數據信息等,後續可以轉人工覈對該數據),這種方案更加穩定和適用
  • 如果業務邏輯執行失敗,那麼是否應該刪除之前創建的冪等記錄?

    • 按照嚴格的冪等含義來說,我們應該保留這條冪等記錄,並且將冪等記錄的狀態修改爲Exception或failed,後續有重試請求進來時執行返回failed給調用方即可(保證多次調用得到的結果相同)。

      • 但是在真實環境中業務執行異常有可能是數據校驗失敗、接口裏調用外部系統失敗(比如外部系統正在發版(沒有做優雅發佈)等)
      • 針對於這些情況可能調用方修改數據後進行重試或過一定時間後進行重試,那麼此時最好有一定的自愈能力,而不是每次這種數據都轉人工處理(一些場景中會加大人力成本,比如我之前涉及到的某個系統,經常有些調用方傳遞的業務參數有問題或接口裏調用外部系統失敗的情況)
      • 當然這兩種策略需要根據具體的情況來選擇,沒有誰好誰壞之分。
  • 是否需要設定【最大的處理時間】,比如我們期望接口最大處理時間爲1小時(也就是說冪等記錄處理processing狀態的時間最大爲1h),如果超過這個時間,那麼認爲這不是一種正常的case,下次重試請求時應該嘗試恢復業務執行。

    • 這也是個具有兩面性的選擇問題,需要根據實際項目情況權衡選擇
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章