聊聊冪等

聊聊冪等

  • 什麼是冪等
  • 幾個注意點
  • 發生異常時,要不要冪等?
  • 冪等的依據是什麼?
  • 冪等時只需要返回返回碼嗎?
  • 冪等判斷需要控制併發嗎?
  • 其它
  • 常用實踐
  • 利用分佈式鎖+緩存做冪等
  • 利用分佈式鎖+數據庫做冪等
  • 利用數據庫唯一鍵做冪等
  • 其它

聊聊冪等

我們先看看這樣一個問題。

如果你去食堂喫飯。你衝食堂窗口裏喊了一聲:“師傅,倆包子!”等了半天,你還沒見着你的包子。於是,你又衝食堂窗口裏喊了一聲:“師傅,倆包子!”

這時候,你希望食堂師傅怎麼做?

A:給你倆包子。

B:給你四個包子。

C:答應一聲“來了”,但什麼也不給你。

D:端來一碗粥,扣在你臉上。

正常人應該都會選A。這就是冪等。

(說選B的那個,下回我說完兩次“你欠我10塊錢”後,請你記得還我20。)

什麼是冪等

冪等的各種數學定義我們就不討論了。一般來說,我們都會這樣定義冪等:

冪等是指:在其它條件不變的情況下,用同樣的數據、執行同樣的方法,執行一次和執行一萬次,都能得到同樣的結果。

例如,在沒有人修改數據庫數據的情況下執行查詢操作,用同樣的SQL查詢一次和查詢一萬次,都應該能得到同樣的數據。

幾個注意點

冪等操作看起來簡單,其實有很多需要特別注意的地方。

發生異常時,要不要冪等?

我們來看這樣的場景:如果執行某方法時,方法內部拋出了異常;然後,我們修復了方法內部的問題;此時,調用方再來調用,如果按冪等的要求,我們難道要再給調用方拋出一個異常、或者返回對應的錯誤碼嗎?

這個問題,除了考慮冪等之外,我們還需要考慮一點:業務操作的事務性,即業務事務。

業務事務與數據庫事務類似,同樣應當有原子性、一致性、隔離性和持久性。因爲這個緣故,我們常常會把一個業務事務綁定到一個數據庫事務上:只要數據庫事務提交成功了,這個業務事務就成功了。

不過,業務事務的範疇比數據庫事務要更大一些:它不僅包括這個業務操作中的數據庫操作,還可能包括三方接口調用、MQ消息發送、緩存寫操作等其它操作。當然,要保證所有操作、尤其是分佈式操作的事務性四要素,在具體實現上會變得非常複雜。但是,我們至少在設計層面上、至少在數據一致性上,要保證整個業務操作的事務性。

那麼,從業務事務的數據一致性角度來考慮,當一個業務操作的中途拋出異常的時候,我們應當怎麼辦呢?

顯然,我們應當回滾當前業務事務,撤銷異常前的所有操作結果。否則的話,異常前半部分操作成功,寫入了部分數據;異常後半部分沒有執行,沒有寫入數據。此時,就會出現數據不一致問題。

回滾完成後,對我們的系統來說,這個請求的執行次數是0次。此時,調用方再用同樣的數據來調用我們——雖然對調用方來說,這是第二次請求;但是對我們來說,從業務事務和冪等的角度來說,這就是第一次調用。

既然是第一次請求,那就直接放進來,正常操作就好了。

冪等的依據是什麼?

冪等有三個條件:其它條件不變、同樣的數據、同樣的方法。這三個條件中,“同樣的數據”是最難判斷的:調用方傳過來一個a=1,我們怎麼知道這是一筆新的業務操作、還是某次操作的重放呢?

在很多接口的請求報文中,我們都定義了“請求流水號”這樣一個字段。大多數時候,我們就直接用這個字段來做冪等依據。

這種處理方式確實簡單易行。但它其實是有隱患的:即使兩次請求的業務數據完全不同,但只要流水號相同,那第二次請求就是冪等請求;即使兩次請求的業務數據完全相同,但只要流水號不同,那第二次請求就是一次新的請求。

這個問題的直接原因是我們把重要的業務約束交給了很可能不可控、不可靠的調用方。根子上來說,這個問題的原因在於這個“請求流水號”是一個業務無關的“唯一鍵”:它並不能真正用來唯一的標識一筆“業務數據”。

所以,比較嚴謹的冪等方式是找出請求數據中的業務唯一鍵,以業務唯一鍵爲依據來做冪等。有時候唯一鍵需要組合太多字段,做唯一判斷時不好處理,我們也可以把這些字段拼接起來、計算一次MD5:這個MD5,雖然無法回溯到業務數據上,但它的確是一個業務相關的鍵值。

不過,這種業務唯一鍵確實不好找,有的時候業務邏輯確實沒有唯一性:我確實可以在喫完倆包子後,又去食堂窗口喊一次“師傅,倆包子”;甚至喫完這倆之後還可以再來一次“師傅,倆包子”“。而在這種情況下,師傅確實應該給我總共六個包子。

這種情況下就沒什麼辦法了,雖然不是最佳方案,但使用接口傳入的流水號、唯一鍵等特殊字段作爲冪等依據總歸也是一種方案。只不過這種時候,我們必須嚴格要求調用方按約束傳值。

冪等時只需要返回返回碼嗎?

大多數接口的返回報文中,都有code和msg字段,用來標誌本次操作的狀態。對這種接口來說,冪等時顯然只返回code和msg就夠了。

需要注意的是,這裏的code和msg應當和第一次正常請求時的一樣。如果正常請求時響應code=0,冪等請求時響應code=1,那這兩次請求得到的就不是“同樣的結果”了——就跟你衝食堂喊兩次“師傅,倆包子”之後,師傅先給你倆包子、再往你臉上扣一碗粥是一回事兒。

但是,也有很多接口,除了返回code和msg之外,還需要返回業務數據。對這種接口來說,僅僅返回code顯然就不夠了——否則,不就等於是你衝食堂窗口喊“師傅,倆包子”,而師傅只答應“來了”卻不給你包子嗎?

無論哪種情況,核心都是要保證冪等請求和正常請求返回同樣的結果。

冪等判斷需要控制併發嗎?

大多數情況下,我們都需要通過根據請求唯一鍵查詢某個數據來判斷冪等:如果已有數據,說明是冪等請求;如果沒有數據,說明不是冪等請求。

這個邏輯基本可行,除了發生併發請求時:此時,對併發進來的兩個請求來說,用請求中的唯一鍵都查不到數據,因而他們都會被判定爲冪等請求。這樣一來,就等同於一筆請求執行了兩次,也就宣告了冪等判斷失敗了。

所以,如果用這種查詢的方式來判斷冪等,請一定要注意控制併發。

其它

還有什麼問題,大家可以一併提出來討論。

常用實踐

利用分佈式鎖+緩存做冪等

針對請求中的唯一鍵,先加分佈式鎖,後判斷緩存中是否有值。如果沒有值,則執行正常請求,並把正常返回結果(無論成功還是失敗)存入緩存;如果有值,則直接返回緩存結果。

這種方式最高效、最簡單。但是,這種方式有兩個問題。

首先,如果緩存失效了,那麼冪等判斷也就失效了。當然,這種情況比較可控、概率較小。

第二,如果“其它條件不變”這個前提被打破了,緩存數據往往就會“過時”。此時,冪等結果應當返回“其它條件不變”之前的結果,還是返回之後的結果?這是一個值得認真考慮的問題。

例如,query方法天然冪等;但這是建立在數據庫數據沒有被更新的前提之下的。如果query操作有緩存,而且數據庫被更新後沒有即使更新緩存,那麼query操作應該返回緩存裏的結果?還是應該返回數據庫中的實際結果?

同理,如果操作A上有緩存,它的冪等建立在其基礎數據沒有變更的前提下。然而,操作B會更新操作A的基礎數據、但不會更新操作A上的緩存;此時如果做過操作A之後、再做一次操作B,然後再做操作A,操作A應該返回緩存中沒有被操作B修改過的結果、還是返回數據庫中被操作B修改過的結果?

利用分佈式鎖+數據庫做冪等

與緩存相比,數據庫是更可靠的持久化工具。並且,絕大多數業務操作最終都要把結果存入數據庫中。因此,使用數據庫來判斷冪等,也比使用緩存更加可靠一些。

只不過在這種情況下,查到數據庫中有數據、並判定爲冪等之後,往往還需要我們手動把庫中的業務數據再組裝爲接口返回結果。而且,這種方法會帶來額外的一次數據庫查詢操作,如果接口壓力太大,對數據庫性能的影響也不可小覷。

利用數據庫唯一鍵做冪等

數據庫的正常增刪改查操作中,只有增是天然不冪等的(“累計型”的update不算“正常”改操作)。但是,我們可以通過爲數據庫表增加唯一索引來把它轉化爲冪等操作。

不過,使用這種方式時,我們需要捕獲數據庫拋出的唯一鍵衝突異常,並把這個異常處理爲冪等結果——有時也需要再組裝一次返回結果。

這種方式不需要做分佈式鎖處理,而且比第二種方式少一次數據庫操作。不過也需要手動轉一次結果,另外對數據庫的性能也友好不到哪兒去——唯一索引也會拖累insert的性能。

其它

如果還有其它方式,不妨提出來一起討論下。

景昕的花園.png

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章