原文地址:聊聊開發中冪等性問題
冪等 (idempotence) 的概念
冪等的數學概念
冪等是源於一種數學概念。其主要有兩個定義
如果在一元運算中,x 為某集合中的任意數,如果滿足 f(x) = f(f(x)) ,那麼該 f 運算具有冪等性,比如絕對值運算 abs(a) = abs(abs(a)) 就是冪等性函式。
如果在二元運算中,x 為某集合中的任意數,如果滿足 f(x,x) = x,前提是 f 運算的兩個引數均為 x,那麼我們稱 f 運算也有冪等性,比如求大值函式 max(x,x) = x 就是冪等性函式。
冪等性在開發中的概念
在數學中冪等的概念或許比較抽象,但是在開發中冪等性是極為重要的。簡單來說,對於同一個系統,在同樣條件下,一次請求和重複多次請求對資源的影響是一致的,就稱該操作為冪等的。比如說如果有一個介面是冪等的,當傳入相同條件時,其效果必須是相同的。
特別是對於現在分散式系統下的 RPC 或者 Restful 介面互相呼叫的情況下,很容易出現由於網路錯誤等等各種原因導致呼叫的時候出現異常而需要重試,這時候就必須保證介面的冪等性,否則重試的結果將與第一次呼叫的結果不同,如果有個介面的呼叫鏈 A->B->C->D->E,在 D->E 這一步發生異常重試後返回了錯誤的結果,A,B,C也會受到影響,這將會是災難性的。
在生活中常見的一些要求冪等性的例子:
- 部落格系統同一個使用者對同一個文章點贊,即使這人單身30年手速瘋狂按點贊,那麼實際上也只能給這個文章 +1 贊
- 在微信支付的時候,一筆訂單應當只能扣一次錢,那麼無論是網路問題或者bug等而重新付款,都只應該扣一次錢
冪等性與併發安全
在查閱網路資料的時候,我看到許多文章把冪等性和併發安全的問題有些混淆了。冪等性是系統介面對外的一種承諾,而不是實現,承諾多次相同的操作的結果都會是一樣的。而併發安全問題是當多個執行緒同時對同一個資源操作時,由於操作順序等原因導致結果不正確。
這兩個實際上是完全獨立的兩個問題,比如說同一筆訂單即使你不停的提交支付,如果扣除了多次錢,就說明該操作不冪等。而有多筆訂單同時進行支付,最後扣除金額不是這多筆金額的總和,那麼說明該操作有併發安全問題。所以冪等性和併發安全是完全兩個維度的問題,要分開討論解決。
我在一些討論冪等性的文章中看到中給出的解決方案為‘悲觀鎖’和‘樂觀鎖’,這兩個方案可以很好的解決併發問題,但是卻不應該是冪等性問題的解決方案,特別是悲觀鎖是用於防止多個執行緒同時修改一個資源的。倒是樂觀鎖的版本號機制可以勉強以 token
或者狀態標識
作為版本號來實現冪等性(下文解釋token
和狀態標識
),勉強說的過去。
所以說冪等性與併發安全是不同的,在本文就只討論冪等性的問題,對於併發安全問題不做討論
Http 協議與冪等性
如果把操作按照功能分類,那就是增刪改查四種,在 http 協議中則表現為 Get、Post、Put、Delete 四種。
查詢操作 (Get)
Get 方法用於獲取資源,不應當對系統資源進行改變,所以是冪等的。注意這裡的冪等提現在對系統資源的改變,而不是返回資料的結果,即使返回結果不相同但是該操作本身沒有副作用,所以冪等。
刪除操作 (Delete)
Delete 方法用於刪除資源,雖然改變了系統資源,但是第一次和第N次刪除操作對系統的作用是相同的,所以是冪等的。比如要刪除一個 id 為 1234 的資源,可能第一次呼叫時會刪除,而後面所有呼叫的時候由於系統中已經沒有這個 id 的資源了,但是第一次操作和後面的操作對系統的作用是相同的,所以這也是冪等的,呼叫者可以多次呼叫這個介面不必擔心錯誤。
修改操作 (Put)
修改操作有可能是冪等的也可能不冪等。如果修改的資源為固定的,比如說把賬戶中金額改為 1000 元,無論呼叫幾次都是冪等的。假如資源不固定,比如賬戶中金額減少50元,呼叫一次和呼叫多次的結果肯定不一樣,這時候就不冪等了。在修改操作中想要冪等在下文中討論。
新增操作 (Post)
Post 新增操作天生就不是一個冪等操作,其在 http 協議的定義如下:
The POST method is used to request that the origin server accept the entity enclosed in the request as a new subordinate of the resource identified by the Request-URI in the Request-Line.
在其定義中表明瞭 Post 請求用於建立新的資源,這意味著每次呼叫都會在系統中產生新的資源,所以該操作註定不是冪等操作。這時候想要冪等就必須在業務中實現,方案在下文會討論。
實現冪等性的方案
在上面提到的冪等性還是比較理論,下面結合一些常見的實際業務場景來討論冪等性設計方案。
去重表
利用資料庫的特性來實現冪等。通常是在表上構建一個唯一索引,那麼只要某一個資料構建完畢,後面再次操作也無法成功寫入。
常見的業務就是部落格系統點贊功能,一個使用者對一個博文點贊後,就把使用者 id 與 博文 id 繫結,後續該使用者點贊同一個博文就無法插入了。或是在金融系統中,給使用者建立金融賬戶,一個使用者肯定不能有多個賬戶,就在賬戶表中增加唯一索引來儲存使用者 id,這樣即使重複操作使用者也只能擁有一個賬戶。
狀態標識
狀態標識是很常見的冪等設計方式,主要思路就是通過狀態標識的變更,保證業務中每個流程只會在對應的狀態下執行,如果標識已經進入下一個狀態,這時候來了上一個狀態的操作就不允許變更狀態,保證了業務的冪等性。
狀態標識經常用在業務流程較長,修改資料較多的場景裡。最經典的例子就是訂單系統,假如一個訂單要經歷 建立訂單 -> 訂單支付\取消 -> 賬戶計算 -> 通知商戶 這四個步驟。那麼就有可能一筆訂單支付完成後去賬戶里扣除對應的餘額,消耗對應的優惠卷。但是由於網路等原因返回了錯誤資訊,這時候就會重試再次去進行賬戶計算步驟造成資料錯誤。
所以為了保證整個訂單流程的冪等性,可以在訂單資訊中增加一個狀態標識,一旦完成了一個步驟就修改對應的狀態標識。比如訂單支付成功後,就把訂單標識為修改為支付成功,現在再次呼叫訂單支付或者取消介面,會先判斷訂單狀態標識,如果是已經支付過或者取消訂單,就不會再次支付了。
Token 機制
Token 機制應該是適用範圍最廣泛的一種冪等設計方案了,具體實現方式也很多樣化。但是核心思想就是每次操作都生成一個唯一 Token 憑證,伺服器通過這個唯一憑證保證同樣的操作不會被執行兩次。這個 Token 除了字面形式上的唯一字串,也可以是多個標誌的組合(比如上面提到的狀態標誌),甚至可以是時間段標識等等。
舉個例子,在論壇中釋出一個新帖子,這是一個典型的 Post 新增操作,要怎樣防止使用者多次點選提交導致產生多個同樣的帖子呢。可以讓使用者提交的時候帶一個唯一 Token,伺服器只要判斷該 Token 存在了就不允許提交,便能保證冪等性。
上面這個例子比較容易理解,但是業務比較簡單。由於 Token 機制適用較廣,所以其設計中要注意的要求也會根據業務不同而不同。
Token 在何時生成,怎麼生成?這是該機制的核心,就拿上面論壇系統來說,如果你在使用者提交帖子的時候才生成 Token,那使用者每次點提交都會生成新的 Token 然後都能提交成功,就不是冪等的了。必須在使用者提交內容之前,比如進入編輯頁面的時候生成 Token,使用者在提交的時候內容帶著 Token 一起提交,對於同一個頁面無論使用者提交多少次,就至多能成功一次。所以 Token 生成的時機必須保證能夠使該操作具多次執行都是相同的效果才行。使用 Token 機制就要求開發者對業務流程有較好的理解。
結語
冪等性是開發當中很常見也很重要的一個需求。尤其是金融、支付等行業對其要求更加嚴格,既要有好的效能也要有嚴格的冪等性。除了對其概念的掌握,理解自身業務需求更是實現冪等功能的要點,必須處理好每一個結點細節,一旦某個地方沒有設計完善,最後的結果可能仍舊達不到要求。