冪等設計

最近作的項目的性能調優中關於冪等設計的一些總結html

場景:假設有這樣一個方法,包含了一些DB操做,check if existing then update else save. 若是兩個線程同時去執行這個方法,而且他們處理的是同一條數據,指望應該是其中一個線程是save,另一個是update。可是有可能線程的處理時間至關重合,線程A在check的時候,線程B也在check,這時A和B都認爲數據不存在,都去save,在數據庫有unique 約束的狀況下其中一個操做會失敗,而咱們指望的多是後面一個操做應該update(取決於具體業務)。前端

這是很典型的多線程問題,check - then do something,在單系統環境中這很容易用線程同步來處理(syncronised). 可是若是是分佈式系統,這兩個線程在不一樣的server上面,syncronised 是不會起效的,並且同步每每下降效率,並非咱們想要的。node

擁有相同參數的屢次請求對系統形成的反作用應該是相同的,這就是冪等性。在這個例子裏面就是說保證相同的ID組合只會插入一條數據到DB裏面,若是一個請求是save,後續的都應該update這條。在單系統中也能夠用冪等的設計來規避使用syncronized,由於那會下降效率。通常狀況下數據庫就能保證這種冪等性--用unique關鍵字,以上面的場景爲例,假如其中一個線程的save操做失敗,咱們能夠用catch 特定的exception而後判斷是否是要進行update操做的方式來試圖保證冪等。在分佈式系統中更應該考慮冪等設計,尤爲是高併發,高性能要求下。併發量高的狀況下,線程的衝突很容易發生。即便是小几率時間,也是必然會發生的。mysql

咱們系統的實際需求是須要在一個方法裏面作很大DB操做,其中就包括了check if existing then update else save,開始的時候咱們估計重複數據的機率很小,可是在後來的性能測試中發現仍是會出現,咱們的測試併發量是30000 TPS左右,有8個node。nginx

分佈式系統中必定要考慮冪等的設計,由於相較於進行分佈式事務設計,冪等設計輕量級的多。程序員

 

1. 若是接入的是服務端,能夠由服務端確保生成惟一的標識符redis

2. 若是是接入最終用戶的瀏覽器,則能夠由本身的服務器先生成一個標識符發送給瀏覽器,當用戶提交表單的時候,以此來認證是否爲二次提交。sql

3. 若是確認爲二次提交,則把第一次的處理結果再次返給請求端數據庫

 分佈式系統---冪等性設計編程

 

  WEB資源或API方法的冪等性是指一次和屢次請求某一個資源應該具備一樣的反作用。冪等性是系統的接口對外一種承諾(而不是實現), 承諾只要調用接口成功, 外部屢次調用對系統的影響是一致的。冪等性是分佈式系統設計中的一個重要概念,對超時處理、系統恢復等具備重要意義。聲明爲冪等的接口會認爲外部調用失敗是常態, 而且失敗以後必然會有重試。例如,在因網絡中斷等緣由致使請求方未能收到請求返回值的狀況下,若是該資源具有冪等性,請求方只須要從新請求便可,而無需擔憂重複調用會產生錯誤。實際上,咱們經常使用的HTTP協議的方法是具備冪等性語義要求的,好比:get方法用於獲取資源,不該有反作用,所以是冪等的;post方法用於建立資源,每次請求都會產生新的資源,所以不具有冪等性;put方法用於更新資源,是冪等的;delete方法用於刪除資源,也是冪等的。

常見用來保證冪等的手段:

1.MVCC方案   多版本併發控制,該策略主要使用update with condition(更新帶條件來防止)來保證屢次外部請求調用對系統的影響是一致的。在系統設計的過程當中,合理的使用樂觀鎖,經過version或者updateTime(timestamp)等其餘條件,來作樂觀鎖的判斷條件,這樣保證更新操做即便在併發的狀況下,也不會有太大的問題。例如

?
1
2
select * from tablename where condition=#condition# //取出要跟新的對象,帶有版本versoin
update tableName set name=#name#,version=version+1 where version=#version#

  在更新的過程當中利用version來防止,其餘操做對對象的併發更新,致使更新丟失。爲了不失敗,一般須要必定的重試機制。

2.去重表   在插入數據的時候,插入去重表,利用數據庫的惟一索引特性,保證惟一的邏輯。

3.悲觀鎖

  select for update,整個執行過程當中鎖定該訂單對應的記錄。注意:這種在DB讀大於寫的狀況下儘可能少用。

4. select + insert   併發不高的後臺系統,或者一些任務JOB,爲了支持冪等,支持重複執行,簡單的處理方法是,先查詢下一些關鍵數據,判斷是否已經執行過,在進行業務處理,就能夠了。注意:核心高併發流程不要用這種方法。

5.狀態機冪等   在設計單據相關的業務,或者是任務相關的業務,確定會涉及到狀態機,就是業務單據上面有個狀態,狀態在不一樣的狀況下會發生變動,通常狀況下存在有限狀態機,這時候,若是狀態機已經處於下一個狀態,這時候來了一個上一個狀態的變動,理論上是不可以變動的,這樣的話,保證了有限狀態機的冪等。

6. token機制,防止頁面重複提交

  業務要求:頁面的數據只能被點擊提交一次   發生緣由:因爲重複點擊或者網絡重發,或者nginx重發等狀況會致使數據被重複提交 解決辦法:

  • 集羣環境:採用token加redis(redis單線程的,處理須要排隊)
  • 單JVM環境:採用token加redis或token加jvm內存

處理流程:

  • 數據提交前要向服務的申請token,token放到redis或jvm內存,token有效時間
  • 提交後後臺校驗token,同時刪除token,生成新的token返回

  token特色:要申請,一次有效性,能夠限流 

7. 對外提供接口的api如何保證冪等 

  如銀聯提供的付款接口:須要接入商戶提交付款請求時附帶:source來源,seq序列號。source+seq在數據庫裏面作惟一索引,防止屢次付款,(併發時,只能處理一個請求)

  總結: 冪等性應該是合格程序員的一個基因,在設計系統時,是首要考慮的問題,尤爲是在像支付寶,銀行,互聯網金融公司等涉及的都是錢的系統,既要高效,數據也要準確,因此不能出現多扣款,多打款等問題,這樣會很難處理,用戶體驗也很差 。

 

冪等概念來自數學,表示N次變換和1次變換的結果是相同的。這裏討論在某些場景下,客戶端在調用服務沒有達到預期結果時,會進行屢次調用,爲避免屢次重複的調用對服務資源產生反作用,服務提供者會承諾知足冪等。

舉個栗子,雙十一零點剛過,小明就火燒眉毛地點擊提交訂單按鈕,選擇在線支付,點了確認支付按鈕,這時候網絡有些慢,小明擔憂心愛的商品被搶購一空,就點了屢次確認付款按鈕,若是這個訂單扣款屢次,客服熱線估計會被小明打爆。

什麼是冪等性

HTTP/1.1中對冪等性的定義是:一次和屢次請求某一個資源對於資源自己應該具備一樣的反作用(網絡超時等問題除外)。也就是說,其任意屢次執行對資源自己所產生的影響均與一次執行的影響相同

Methods can also have the property of 「idempotence」 in that (aside from error or expiration issues) the side-effects of N > 0 identical requests is the same as for a single request.

這裏須要關注幾個重點:

  1. 冪等不只僅只是一次(或屢次)請求對資源沒有反作用(好比查詢數據庫操做,沒有增刪改,所以沒有對數據庫有任何影響)。
  2. 冪等還包括第一次請求的時候對資源產生了反作用,可是之後的屢次請求都不會再對資源產生反作用。
  3. 冪等關注的是之後的屢次請求是否對資源產生的反作用,而不關注結果。
  4. 網絡超時等問題,不是冪等的討論範圍。

冪等性是系統服務對外一種承諾(而不是實現),承諾只要調用接口成功,外部屢次調用對系統的影響是一致的。聲明爲冪等的服務會認爲外部調用失敗是常態,而且失敗以後必然會有重試。

什麼狀況下須要冪等

業務開發中,常常會遇到重複提交的狀況,不管是因爲網絡問題沒法收到請求結果而從新發起請求,或是前端的操做抖動而形成重複提交狀況。 在交易系統,支付系統這種重複提交形成的問題有尤爲明顯,好比:

  1. 用戶在APP上連續點擊了屢次提交訂單,後臺應該只產生一個訂單;
  2. 向支付寶發起支付請求,因爲網絡問題或系統BUG重發,支付寶應該只扣一次錢。 很顯然,聲明冪等的服務認爲,外部調用者會存在屢次調用的狀況,爲了防止外部屢次調用對系統數據狀態的發生屢次改變,將服務設計成冪等。

冪等VS防重

上面例子中小明遇到的問題,只是重複提交的狀況,和服務冪等的初衷是不一樣的。重複提交是在第一次請求已經成功的狀況下,人爲的進行屢次操做,致使不知足冪等要求的服務屢次改變狀態。而冪等更多使用的狀況是第一次請求不知道結果(好比超時)或者失敗的異常狀況下,發起屢次請求,目的是屢次確認第一次請求成功,卻不會因屢次請求而出現屢次的狀態變化。

什麼狀況下須要保證冪等性

以SQL爲例,有下面三種場景,只有第三種場景須要開發人員使用其餘策略保證冪等性:

  1. SELECT col1 FROM tab1 WHER col2=2,不管執行多少次都不會改變狀態,是自然的冪等。
  2. UPDATE tab1 SET col1=1 WHERE col2=2,不管執行成功多少次狀態都是一致的,所以也是冪等操做。
  3. UPDATE tab1 SET col1=col1+1 WHERE col2=2,每次執行的結果都會發生變化,這種不是冪等的。

爲何要設計冪等性的服務

冪等可使得客戶端邏輯處理變得簡單,可是卻以服務邏輯變得複雜爲代價。知足冪等服務的須要在邏輯中至少包含兩點:

  1. 首先去查詢上一次的執行狀態,若是沒有則認爲是第一次請求
  2. 在服務改變狀態的業務邏輯前,保證防重複提交的邏輯

冪等的不足

冪等是爲了簡化客戶端邏輯處理,卻增長了服務提供者的邏輯和成本,是否有必要,須要根據具體場景具體分析,所以除了業務上的特殊要求外,儘可能不提供冪等的接口。

  1. 增長了額外控制冪等的業務邏輯,複雜化了業務功能;
  2. 把並行執行的功能改成串行執行,下降了執行效率。

保證冪等策略

冪等須要經過惟一的業務單號來保證。也就是說相同的業務單號,認爲是同一筆業務。使用這個惟一的業務單號來確保,後面屢次的相同的業務單號的處理邏輯和執行效果是一致的。 下面以支付爲例,在不考慮併發的狀況下,實現冪等很簡單:①先查詢一下訂單是否已經支付過,②若是已經支付過,則返回支付成功;若是沒有支付,進行支付流程,修改訂單狀態爲‘已支付’。

防重複提交策略

上述的保證冪等方案是分紅兩步的,第②步依賴第①步的查詢結果,沒法保證原子性的。在高併發下就會出現下面的狀況:第二次請求在第一次請求第②步訂單狀態尚未修改成‘已支付狀態’的狀況下到來。既然得出了這個結論,餘下的問題也就變得簡單:把查詢和變動狀態操做加鎖,將並行操做改成串行操做。

樂觀鎖

若是隻是更新已有的數據,沒有必要對業務進行加鎖,設計表結構時使用樂觀鎖,通常經過version來作樂觀鎖,這樣既能保證執行效率,又能保證冪等。例如: UPDATE tab1 SET col1=1,version=version+1 WHERE version=#version# 不過,樂觀鎖存在失效的狀況,就是常說的ABA問題,不過若是version版本一直是自增的就不會出現ABA的狀況。(從網上找了一張圖片很能說明樂觀鎖,引用過來,出自Mybatis對樂觀鎖的支持

防重表

使用訂單號orderNo作爲去重表的惟一索引,每次請求都根據訂單號向去重表中插入一條數據。第一次請求查詢訂單支付狀態,固然訂單沒有支付,進行支付操做,不管成功與否,執行完後更新訂單狀態爲成功或失敗,刪除去重表中的數據。後續的訂單由於表中惟一索引而插入失敗,則返回操做失敗,直到第一次的請求完成(成功或失敗)。能夠看出防重表做用是加鎖的功能。

分佈式鎖

這裏使用的防重表可使用分佈式鎖代替,好比Redis。訂單發起支付請求,支付系統會去Redis緩存中查詢是否存在該訂單號的Key,若是不存在,則向Redis增長Key爲訂單號。查詢訂單支付已經支付,若是沒有則進行支付,支付完成後刪除該訂單號的Key。經過Redis作到了分佈式鎖,只有此次訂單訂單支付請求完成,下次請求才能進來。相比去重表,將放併發作到了緩存中,較爲高效。思路相同,同一時間只能完成一次支付請求

token令牌

這種方式分紅兩個階段:申請token階段和支付階段。 第一階段,在進入到提交訂單頁面以前,須要訂單系統根據用戶信息向支付系統發起一次申請token的請求,支付系統將token保存到Redis緩存中,爲第二階段支付使用。 第二階段,訂單系統拿着申請到的token發起支付請求,支付系統會檢查Redis中是否存在該token,若是存在,表示第一次發起支付請求,刪除緩存中token後開始支付邏輯處理;若是緩存中不存在,表示非法請求。 實際上這裏的token是一個信物,支付系統根據token確認,你是你媽的孩子。不足是須要系統間交互兩次,流程較上述方法複雜。

支付緩衝區

把訂單的支付請求都快速地接下來,一個快速接單的緩衝管道。後續使用異步任務處理管道中的數據,過濾掉重複的待支付訂單。優勢是同步轉異步,高吞吐。不足是不能及時地返回支付結果,須要後續監聽支付結果的異步返回。

我是葛一凡,但願對你有幫助。 微信公衆號

參考

    1. 高併發的核心技術-冪等的實現方案
    2. 防重複請求處理的實踐與總結
    3. 分佈式服務協調—冪等(Idempotent)機制
    4. 分佈式系統接口冪等性
    5. 冪等性 我的理解及應用
    6. 編程中的冪等性 —— HTTP冪等性
    7. 系統冪等以及經常使用實現方式
    8. 高併發系統數據冪等的技術嘗試
    9. Mybatis對樂觀鎖的支持
相關文章
相關標籤/搜索