由防重复点击引发的幂等性问题思考
HTTP/1.1中對冪等性的定義是:一次和多次請求某一個資源對于資源本身應該具有同樣的結果(網絡超時等問題除外)。也就是說,用戶對于同一操作發起的一次請求或者多次請求的結果是一致的,不會因為多次點擊而產生了副作用。舉個最簡單的例子,那就是支付,用戶購買商品使用約支付,支付扣款成功,但是返回結果的時候網絡異常,此時錢已經扣了,用戶再次點擊按鈕,此時會進行第二次扣款,返回結果成功,用戶查詢余額返發現多扣錢了,流水記錄也變成了兩條.
這里需要關注幾個重點:
1、冪等不僅僅只是一次(或多次)請求對資源沒有副作用(比如查詢數據庫操作,沒有增刪改,因此沒有對數據庫有任何影響)。
2、冪等還包括第一次請求的時候對資源產生了副作用,但是以后的多次請求都不會再對資源產生副作用。
3、冪等關注的是以后的多次請求是否對資源產生的副作用,而不關注結果。
4、網絡超時等問題,不是冪等的討論范圍。
冪等性是系統服務對外一種承諾(而不是實現),承諾只要調用接口成功,外部多次調用對系統的影響是一致的。聲明為冪等的服務會認為外部調用失敗是常態,并且失敗之后必然會有重試。
那么我們為什么需要接口具有冪等性呢?設想一下以下情形:
- 在App中下訂單的時候,點擊確認之后,沒反應,就又點擊了幾次。在這種情況下,如果無法保證該接口的冪等性,那么將會出現重復下單問題。
- 在接收消息的時候,消息推送重復。如果處理消息的接口無法保證冪等,那么重復消費消息產生的影響可能會非常大。
- 在分布式環境中,網絡環境更加復雜,因前端操作抖動、網絡故障、消息重復、響應速度慢等原因,對接口的重復調用概率會比集中式環境下更大,尤其是重復消息在分布式環境中很難避免。
分布式環境中,有些接口是天然保證冪等性的,如查詢操作。有些對數據的修改是一個常量,并且無其他記錄和操作,那也可以說是具有冪等性的。其他情況下,所有涉及對數據的修改、狀態的變更就都有必要防止重復性操作的發生。通過間接的實現接口的冪等性來防止重復操作所帶來的影響,成為了一種有效的解決方案。
冪等和防重的區別
防重復提交的示例:比如我之前寫的一個針對簽約系統的審批流,因為場景需要,某一個業務可以提交多次審批,不能做是否重復提審的限制,但是會遇到重復提交的問題,比如連續多次點擊提審按鈕。這個問題只是重復提交的情況,和服務冪等的初衷是不同的。
重復提交是在第一次請求已經成功的情況下,人為的進行多次操作,導致不滿足冪等要求的服務多次改變狀態。而冪等更多使用的情況是第一次請求不知道結果(比如超時)或者失敗的異常情況下,發起多次請求,目的是多次確認第一次請求成功,卻不會因多次請求而出現多次的狀態變化。
冪等可以使得客戶端邏輯處理變得簡單,但是卻以服務邏輯變得復雜為代價。滿足冪等服務的需要在邏輯中至少包含兩點:
1、首先去查詢上一次的執行狀態,如果沒有則認為是第一次請求;
2、在服務改變狀態的業務邏輯前,保證防重復提交的邏輯;
保證冪等策略
冪等需要通過唯一的業務單號來保證。也就是說相同的業務單號,認為是同一筆業務。使用這個唯一的業務單號來確保,后面多次的相同的業務單號的處理邏輯和執行效果是一致的。
下面以支付為例,在不考慮并發的情況下,實現冪等很簡單:先查詢一下訂單是否已經支付過,如果已經支付過,則返回支付成功;如果沒有支付,進行支付流程,修改訂單狀態為‘已支付’。
實現冪等性的幾種方案
舉個例子:
有一個訂單系統,對外提供了一個處理接口,如果有個訂單001是要扣除用戶的100塊錢,那么訂單001被多次調用,也只會處理成功一次,也就是只會扣除用戶100塊。也可以理解為去除重復調用。
例如:
等等很多重要的情況,這些邏輯都需要冪等的特性來支持。
實現冪等性的技術方案
查詢一次和查詢多次,在數據不變的情況下,查詢結果是一樣的,select是天然的冪等操作。
刪除操作也是冪等的,刪除一次和多次刪除都是把數據刪除。(注意可能返回結果不一樣,刪除的數據不存在,返回0,刪除的數據多條,返回結果多個)。
3.唯一索引,防止新增臟數據
比如:支付寶的資金賬戶,支付寶也有用戶賬戶,每個用戶只能有一個資金賬戶,怎么防止給用戶創建資金賬戶多個,那么給資金賬戶表中的用戶ID加唯一索引,所以一個用戶新增成功一個資金賬戶記錄。
要點:唯一索引或唯一組合索引來防止新增數據存在臟數據 (當表存在唯一索引,并發時新增報錯時,再查詢一次就可以了,數據應該已經存在了,返回結果即可)。
業務要求:頁面的數據只能被點擊提交一次;
發生原因:由于重復點擊或者網絡重發,或者nginx重發等情況會導致數據被重復提交。
解決辦法:
集群環境:采用token加redis(redis單線程的,處理需要排隊)
單JVM環境:采用token加redis或token加jvm內存
處理流程:
token特點: 要申請,一次有效性,可以限流
注意:redis要用刪除操作來判斷token,刪除成功代表token校驗通過,如果用select+delete來校驗token,存在并發問題,不建議使用
獲取數據的時候加鎖獲取
select * from table_xxx where id=‘xxx’ for update;
注意:id字段一定是主鍵或者唯一索引,不然是鎖表,會出事的。
悲觀鎖使用時一般伴隨事務一起使用,數據鎖定時間可能會很長,根據實際情況選用
樂觀鎖只是在更新數據那一刻鎖表,其他時間不鎖表,所以相對于悲觀鎖,效率更高。樂觀鎖的實現方式多種多樣可以通過version或者其他狀態條件:
- 通過版本號實現
update table_xxx set name=#name#,version=version+1 where version=#version#
- 通過條件限制
update table_xxx set avai_amount=avai_amount-#subAmount# where avai_amount-#subAmount# >= 0
要求:quality-#subQuality# >= ,這個情景適合不用版本號,只更新是做數據安全校驗,適合庫存模型,扣份額和回滾份額,性能更高。
注意:樂觀鎖的更新操作,最好用主鍵或者唯一索引來更新,這樣是行鎖,否則更新時會鎖表,上面兩個sql改成下面的兩個更好。
update table_xxx set name=#name#,version=version+1 where id=#id# and version=#version#
update table_xxx set avai_amount=avai_amount-#subAmount# where id=#id# and avai_amount-#subAmount# >= 0
還是拿插入數據的例子,如果是分布是系統,構建全局唯一索引比較困難,例如唯一性的字段沒法確定,這時候可以引入分布式鎖,通過第三方的系統(redis或zookeeper),在業務系統插入數據或者更新數據,獲取分布式鎖,然后做操作,之后釋放鎖,這樣其實是把多線程并發的鎖的思路,引入多多個系統,也就是分布式系統中得解決思路。
要點:某個長流程處理過程要求不能并發執行,可以在流程執行之前根據某個標志(用戶ID+后綴等)獲取分布式鎖,其他流程執行時獲取鎖就會失敗,也就是同一時間該流程只能有一個能執行成功,執行完成后,釋放分布式鎖(分布式鎖要第三方系統提供)。
并發不高的后臺系統,或者一些任務JOB,為了支持冪等,支持重復執行,簡單的處理方法是,先查詢下一些關鍵數據,判斷是否已經執行過,在進行業務處理,就可以了。
注意:核心高并發流程不要用這種方法。
在設計單據相關的業務,或者是任務相關的業務,肯定會涉及到狀態機(狀態變更圖),就是業務單據上面有個狀態,狀態在不同的情況下會發生變更,一般情況下存在有限狀態機,這時候,如果狀態機已經處于下一個狀態,這時候來了一個上一個狀態的變更,理論上是不能夠變更的,這樣的話,保證了有限狀態機的冪等。
注意:訂單等單據類業務,存在很長的狀態流轉,一定要深刻理解狀態機,對業務系統設計能力提高有很大幫助。
如銀聯提供的付款接口:需要接入商戶提交付款請求時附帶:source來源,seq序列號,source+seq在數據庫里面做唯一索引,防止多次付款,(并發時,只能處理一個請求)。
重點:
對外提供接口為了支持冪等調用,接口有兩個字段必須傳,一個是來源source,一個是來源方序列號seq,這個兩個字段在提供方系統里面做聯合唯一索引,這樣當第三方調用時,先在本方系統里面查詢一下,是否已經處理過,返回相應處理結果;沒有處理過,進行相應處理,返回結果。注意,為了冪等友好,一定要先查詢一下,是否處理過該筆業務,不查詢直接插入業務系統,會報錯,但實際已經處理了。
最后總結:
冪等性應該是合格程序員的一個基因,在設計系統時,是首要考慮的問題,尤其是在像第三方支付平臺,銀行,互聯網金融公司等涉及的網上資金系統,既要高效,數據也要準確,所以不能出現多扣款,多打款等問題,這樣會很難處理,并會大大降低用戶體驗。
那么如何設計接口才能做到冪等呢?
方法一、單次支付請求,也就是直接支付了,不需要額外的數據庫操作了,這個時候發起異步請求創建一個唯一的ticketId,就是門票,這張門票只能使用一次就作廢,具體步驟如下:
1、異步請求獲取門票
2、調用支付,傳入門票
3、根據門票ID查詢此次操作是否存在,如果存在則表示該操作已經執行過,直接返回結果;如果不存在,支付扣款,保存結果
4、返回結果到客戶端
如果步驟4通信失敗,用戶再次發起請求,那么最終結果還是一樣的
方法二、分布式環境下各個服務相互調用
這邊就要舉例我們的系統了,我們支付的時候先要扣款,然后更新訂單,這個地方就涉及到了訂單服務以及支付服務了。用戶調用支付,扣款成功后,更新對應訂單狀態,然后再保存流水。而在這個地方就沒必要使用門票ticketId了,因為會比較閑的麻煩
(支付狀態:未支付,已支付)
步驟:
1、查詢訂單支付狀態
2、如果已經支付,直接返回結果
3、如果未支付,則支付扣款并且保存流水
4、返回支付結果
如果步驟4通信失敗,用戶再次發起請求,那么最終結果還是一樣的。
總結
以上是生活随笔為你收集整理的由防重复点击引发的幂等性问题思考的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MyBatis自定义类型处理器 Type
- 下一篇: 自定义类型处理器的应用