原文地址:聊聊開發中冪等性問題html
冪等是源於一種數學概念。其主要有兩個定義數據庫
若是在一元運算中,x 爲某集合中的任意數,若是知足 f(x) = f(f(x)) ,那麼該 f 運算具備冪等性,好比絕對值運算 abs(a) = abs(abs(a)) 就是冪等性函數。api
若是在二元運算中,x 爲某集合中的任意數,若是知足 f(x,x) = x,前提是 f 運算的兩個參數均爲 x,那麼咱們稱 f 運算也有冪等性,好比求大值函數 max(x,x) = x 就是冪等性函數。安全
在數學中冪等的概念或許比較抽象,可是在開發中冪等性是極爲重要的。簡單來講,對於同一個系統,在一樣條件下,一次請求和重複屢次請求對資源的影響是一致的,就稱該操做爲冪等的。好比說若是有一個接口是冪等的,當傳入相同條件時,其效果必須是相同的。服務器
特別是對於如今分佈式系統下的 RPC 或者 Restful 接口互相調用的狀況下,很容易出現因爲網絡錯誤等等各類緣由致使調用的時候出現異常而須要重試,這時候就必須保證接口的冪等性,不然重試的結果將與第一次調用的結果不一樣,若是有個接口的調用鏈 A->B->C->D->E,在 D->E 這一步發生異常重試後返回了錯誤的結果,A,B,C也會受到影響,這將會是災難性的。微信
在生活中常見的一些要求冪等性的例子:restful
- 博客系統同一個用戶對同一個文章點贊,即便這人單身30年手速瘋狂按點贊,那麼實際上也只能給這個文章 +1 贊
- 在微信支付的時候,一筆訂單應當只能扣一次錢,那麼不管是網絡問題或者bug等而從新付款,都只應該扣一次錢
在查閱網絡資料的時候,我看到許多文章把冪等性和併發安全的問題有些混淆了。冪等性是系統接口對外的一種承諾,而不是實現,承諾屢次相同的操做的結果都會是同樣的。而併發安全問題是當多個線程同時對同一個資源操做時,因爲操做順序等緣由致使結果不正確。網絡
這兩個其實是徹底獨立的兩個問題,好比說同一筆訂單即便你不停的提交支付,若是扣除了屢次錢,就說明該操做不冪等。而有多筆訂單同時進行支付,最後扣除金額不是這多筆金額的總和,那麼說明該操做有併發安全問題。因此冪等性和併發安全是徹底兩個維度的問題,要分開討論解決。併發
我在一些討論冪等性的文章中看到中給出的解決方案爲‘悲觀鎖’和‘樂觀鎖’,這兩個方案能夠很好的解決併發問題,可是卻不該該是冪等性問題的解決方案,特別是悲觀鎖是用於防止多個線程同時修改一個資源的。卻是樂觀鎖的版本號機制能夠勉強以 token
或者狀態標識
做爲版本號來實現冪等性(下文解釋token
和狀態標識
),勉強說的過去。分佈式
因此說冪等性與併發安全是不一樣的,在本文就只討論冪等性的問題,對於併發安全問題不作討論
若是把操做按照功能分類,那就是增刪改查四種,在 http 協議中則表現爲 Get、Post、Put、Delete 四種。
Get 方法用於獲取資源,不該當對系統資源進行改變,因此是冪等的。注意這裏的冪等提如今對系統資源的改變,而不是返回數據的結果,即便返回結果不相同可是該操做自己沒有反作用,因此冪等。
Delete 方法用於刪除資源,雖然改變了系統資源,可是第一次和第N次刪除操做對系統的做用是相同的,因此是冪等的。好比要刪除一個 id 爲 1234 的資源,可能第一次調用時會刪除,然後面全部調用的時候因爲系統中已經沒有這個 id 的資源了,可是第一次操做和後面的操做對系統的做用是相同的,因此這也是冪等的,調用者能夠屢次調用這個接口沒必要擔憂錯誤。
修改操做有多是冪等的也可能不冪等。若是修改的資源爲固定的,好比說把帳戶中金額改成 1000 元,不管調用幾回都是冪等的。假如資源不固定,好比帳戶中金額減小50元,調用一次和調用屢次的結果確定不同,這時候就不冪等了。在修改操做中想要冪等在下文中討論。
2019-08-13 修改原文對Put協議定義有錯誤,Put操做必須爲冪等的,即若是聲明爲Put協議時就至關於對外聲明這個接口是冪等的。因此對於原文舉例說
帳戶中金額減小50元
這種操做在Put協議中是不容許的,只能作相似於帳戶中金額改成 1000 元
的操做
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 除了字面形式上的惟一字符串,也能夠是多個標誌的組合(好比上面提到的狀態標誌),甚至能夠是時間段標識等等。
舉個例子,在論壇中發佈一個新帖子,這是一個典型的 Post 新增操做,要怎樣防止用戶屢次點擊提交致使產生多個一樣的帖子呢。可讓用戶提交的時候帶一個惟一 Token,服務器只要判斷該 Token 存在了就不容許提交,便能保證冪等性。
上面這個例子比較容易理解,可是業務比較簡單。因爲 Token 機制適用較廣,因此其設計中要注意的要求也會根據業務不一樣而不一樣。
Token 在什麼時候生成,怎麼生成?這是該機制的核心,就拿上面論壇系統來講,若是你在用戶提交帖子的時候才生成 Token,那用戶每次點提交都會生成新的 Token 而後都能提交成功,就不是冪等的了。必須在用戶提交內容以前,好比進入編輯頁面的時候生成 Token,用戶在提交的時候內容帶着 Token 一塊兒提交,對於同一個頁面不管用戶提交多少次,就至多能成功一次。因此 Token 生成的時機必須保證可以使該操做具屢次執行都是相同的效果才行。使用 Token 機制就要求開發者對業務流程有較好的理解。
冪等性是開發當中很常見也很重要的一個需求。尤爲是金融、支付等行業對其要求更加嚴格,既要有好的性能也要有嚴格的冪等性。除了對其概念的掌握,理解自身業務需求更是實現冪等功能的要點,必須處理好每個結點細節,一旦某個地方沒有設計完善,最後的結果可能仍舊達不到要求。