分散式系統中的冪等設計
前言
現如今我們的系統大多拆分為分散式SOA,或者微服務,一套系統中包含了多個子系統服務,而一個子系統服務往往會去呼叫另一個服務,而服務呼叫服務無非就是使用RPC通訊或者restful,既然是通訊,那麼就有可能再伺服器處理完畢後返回結果的時候掛掉,這個時候使用者端發現很久沒有反應,那麼就會多次點選按鈕,這樣請求有多次,那麼處理資料的結果是否要統一呢?那是肯定的!尤其再支付場景。冪等性:就是使用者對於同一操作發起的一次請求或者多次請求的結果是一致的,不會因為多次點選而產生了副作用。舉個最簡單的例子,那就是支付,使用者購買商品使用約支付,支付扣款成功,但是返回結果的時候網路異常,此時錢已經扣了,使用者再次點選按鈕,此時會進行第二次扣款,返回結果成功,使用者查詢餘額返發現多扣錢了,流水記錄也變成了兩條。在一個典型的訂單交易系統中,防重和冪等設計是重要而又非常基本的概念。防重是指重複多次提交同樣的交易指令或者訂單請求到後臺,系統必須能夠去重,防止重複執行;而冪等,則是在多個同樣的交易指令或請求同時或者先後到達後臺,即使重複執行,系統也必須始終提供與一致的狀態,而不能有其他的副作用。看起來,防重與冪等似乎在說同一件事情,但其實又有不同的概念區分。例如冪等其實可以通過防重設計來達到提供一致的系統狀態,而防重卻不如冪等那樣開放承諾,允許被執行多次而達到一致狀態,這其實要求在防重的基礎上做出更多的設計工作,特別是在分散式環境中。
一、冪等定義
冪等是資料中得一個概念,表示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.這裡不討論學術上如何定義冪等性,而是重點在於如何在分散式環境中提供對外冪等性的介面。對外提供的介面承諾冪等性,其要表達的含義是:只要呼叫介面成功,外部對介面的多次呼叫得到的結果是相同的。即執行多次和一次的效果是一樣的。其實我們也可以從服務端架構分層上來理解,如果冪等設計放在越前端,那麼提供的其實是一種防重方案;越往後端,隨著業務邏輯的深入,冪等的設計方案也就更加複雜。這其實取決於相關交易系統的業務場景以及部署架構的特點。在實際的生產環境中,我們不時還是會遇到防重或者冪等問題。例如有些老的訂單系統,可能因為某個中間節點阻塞超時,觸發了重發機制,最終導致多個相同ID的交易請求同時到達系統後臺。由於老訂單系統只提供了簡單的序列防重,並沒有充分考慮高併發冪等,結果將同一個使用者的交易請求執行了多次,導致該使用者的前後端的資產份額不一致,最終不得不人工介入解決。所以,有了這個前車之鑑,在我們的X訂單系統開發中,冪等設計第一時間便提上日程,最終完美地解決了老系統的這個設計缺陷。
二、常見的冪等實現方案
1、最簡單的,需要通過唯一的業務單號來保證冪等。也就是說相同的業務單號,認為是同一筆業務。使用這個唯一的業務單號來確保,後面多次的相同的業務單號的處理邏輯和執行效果是一致的。以支付為例,在不考慮併發的情況下,實現冪等很簡單:①先查詢一下訂單是否已經支付過,②如果已經支付過,則返回支付成功;如果沒有支付,進行支付流程,修改訂單狀態為‘已支付’。
2、上述的保證冪等方案是分成兩步的,第②步依賴第①步的查詢結果,無法保證原子性的。在高併發下就會出現下面的情況:第二次請求在第一次請求第②步訂單狀態還沒有修改為‘已支付狀態’的情況下到來。既然得出了這個結論,餘下的問題也就變得簡單:把查詢和變更狀態操作加鎖,將並行操作改為序列操作。
3、但是,在某些場景,你可能又想提供無鎖的高併發冪等,那麼你可以選擇為業務單號加上唯一的索引或者組合索引,在併發的場景中,只有第一筆插入的交易請求能夠成功,後續的請求哪怕是慢1ms或者更短時間,都會觸發資料庫的唯一索引異常而失敗,那麼你可以捕獲這個異常。
4、又或者你想把冪等放在服務的最前端,減少實際服務處理的資源浪費,在請求一到達時就提前去重,不讓他有執行的機會,那麼你可以考慮引入一個redis或類似的元件,將業務請求單號快取在這個分散式鎖的元件內。那麼,每當訂單發起交易請求,交易系統會去Redis快取中查詢是否存在該訂單號的Key,如果不存在,則向Redis增加Key為訂單號。查詢訂單是否已經執行,如果沒有則轉發到交易系統,執行完成後刪除該訂單號的Key。當然,Redis是提供分散式節點下的原子事務操作的。在以前的單應用系統中,我們只需要把資料操作放入事務中即可,發生錯誤立即回滾,但是再響應客戶端的時候也有可能出現網路中斷或者異常等等。在增刪改查4個操作中,尤為注意就是增加或者修改,查詢對於結果是不會有改變的,刪除只會進行一次,使用者多次點選產生的結果一樣。修改在大多場景下結果一樣,增加在重複提交的場景下會出現。
三、保證冪等的常見手段
1.MVCC方案
多版本併發控制,該策略主要使用update with condition(更新帶條件來防止)來保證多次外部請求呼叫對系統的影響是一致的。在系統設計的過程中,合理的使用樂觀鎖,通過version或者updateTime(timestamp)等其他條件,來做樂觀鎖的判斷條件,這樣保證更新操作即使在併發的情況下,也不會有太大的問題。例如: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在資料庫裡面做唯一索引,防止多次付款,(併發時,只能處理一個請求)總結: 冪等性應該是合格程式設計師的一個基因,在設計系統時,是首要考慮的問題,尤其是在像支付寶,銀行,網際網路金融公司等涉及的都是錢的系統,既要高效,資料也要準確,所以不能出現多扣款,多打款等問題,這樣會很難處理,使用者體驗也不好 。
四、高併發系統資料冪等性
在系統開發過程中,經常遇到資料重複插入、重複更新、訊息重發傳送等等問題,因為應用系統的複雜邏輯以及網路互動存在的不確定性,會導致這一重複現象,但是有些邏輯是需要有冪等特性的,否則造成的後果會比較嚴重,例如訂單重複建立,這時候帶來的問題可是非同一般啊。
1、查詢
查詢的API,可以說是天然的冪等性,因為你查詢一次和查詢兩次,對於系統來講,沒有任何資料的變更,所以,查詢一次和查詢多次一樣的;
2、MVCC方案
多版本併發控制,update with condition更新帶條件,這也是在系統設計的時候,合理的選擇樂觀鎖,通過version或者其他條件,來做樂觀鎖,這樣保證更新及時在併發的情況下,也不會有太大的問題。例如:update table_xxx set name=#name#,version=version+1 where version=#version# ,或者是 update table_xxx set quality=quality-#subQuality# where quality-#subQuality# >= 0
3、單獨的去重表
如果涉及到的去重的地方特別多,例如ERP系統中有各種各樣的業務單據,每一種業務單據都需要去重,這時候,可以單獨搞一張去重表,在插入資料的時候,插入去重表,利用資料庫的唯一索引特性,保證唯一的邏輯;
4、分散式鎖
還是拿插入資料的例子,如果是分佈是系統,構建唯一索引比較困難,例如唯一性的欄位沒法確定,這時候可以引入分散式鎖,通過第三方的系統,在業務系統插入資料或者更新資料,獲取分散式鎖,然後做操作,之後釋放鎖,這樣其實是把多執行緒併發的鎖的思路,引入多多個系統,也就是分散式系統中得解決思路;
5、刪除資料
刪除資料,僅僅第一次刪除是真正的操作資料,第二次甚至第三次刪除,直接返回成功,這樣保證了冪等;
6、插入資料的唯一索引
插入資料的唯一性,可以通過業務主鍵來進行約束,例如一個特定的業務場景,三個欄位肯定確定唯一性,那麼,可以在資料庫表新增唯一索引來進行標示。 這裡有一個場景,API層面的冪等,例如提交資料,如何控制重複提交,這裡可以在提交資料的form表單或者客戶端軟體,增加一個唯一標示,然後服務端,根據這個UUID來進行去重,這樣就能比較好的做到API層面的唯一標示
7、狀態機冪等
在設計單據相關的業務,或者是任務相關的業務,肯定會涉及到狀態機,就是業務單據上面有個狀態,狀態在不同的情況下會發生變更,一般情況下存在有限狀態機,這時候,如果狀態機已經處於下一個狀態,這時候來了一個上一個狀態的變更,理論上是不能夠變更的,這樣的話,保證了有限狀態機的冪等
五、做到冪等該如何設計介面?
方法一、單次支付請求,也就是直接支付了,不需要額外的資料庫操作了,這個時候發起非同步請求建立一個唯一的ticketId,就是門票,這張門票只能使用一次就作廢,具體步驟如下:1、非同步請求獲取門票;2、呼叫支付,傳入門票;3、根據門票ID查詢此次操作是否存在,如果存在則表示該操作已經執行過,直接返回結果;如果不存在,支付扣款,儲存結果;4、返回結果到客戶端。如果步驟4通訊失敗,使用者再次發起請求,那麼最終結果還是一樣的
方法二、分散式環境下各個服務相互呼叫。這邊就要舉例我們的系統了,我們支付的時候先要扣款,然後更新訂單,這個地方就涉及到了訂單服務以及支付服務了。使用者呼叫支付,扣款成功後,更新對應訂單狀態,然後再儲存流水。而在這個地方就沒必要使用門票ticketId了,因為會比較閒的麻煩(支付狀態:未支付,已支付),步驟:1、查詢訂單支付狀態;2、如果已經支付,直接返回結果;3、如果未支付,則支付扣款並且儲存流水;4、返回支付結果;如果步驟4通訊失敗,使用者再次發起請求,那麼最終結果還是一樣的對於做過支付的朋友,冪等,也可以稱之為衝正,保證客戶端與服務端的交易一致性,避免多次扣款。