深入理解Java幂等性
深入理解Java冪等性
1. 概念理解
冪等:是一個數學概念,表示N次變換和1次變換的結果相同。
冪等操作:其特點是任意多次執行所產生的影響均與一次執行的影響相同(不會改變資源狀態,對數據沒有副作用)。
冪等性:一系列操作都是冪等操作。
冪等接口:冪等接口認為,外部調用者會存在多次調用的場景,為了防止重試對數據狀態的改變,需要將接口的設計為冪等的
HTTP方法的冪等性是指一次和多次請求某一個資源應該具有同樣的副作用。說白了就是,同一個請求,發送一次和發送N次效果是一樣的!冪等性是分布式系統設計中十分重要的概念,而HTTP的分布式本質也決定了它在HTTP中具有重要地位。下面將以HTTP中的冪等性做例子加以介紹。
另一種解釋:
在編程中.一個冪等操作的特點是其任意多次執行所產生的影響均與一次執行的影響相同。冪等函數,或冪等方法,是指可以使用相同參數重復執行,并能獲得相同結果的函數。這些函數不會影響系統狀態,也不用擔心重復執行會對系統造成改變。例如,“getUsername()和setTrue()”函數就是一個冪等函數. 更復雜的操作冪等保證是利用唯一交易號(流水號)實現.
我的理解:冪等就是一個操作,不論執行多少次,產生的效果和返回的結果都是一樣的。
例子
假設有一個從賬戶取錢的遠程API(可以是HTTP的,也可以不是),我們暫時用類函數的方式記為:
bool withdraw(account_id, amount)
withdraw的語義是從account_id對應的賬戶中扣除amount數額的錢;如果扣除成功則返回true,賬戶余額減少amount;如果扣除失敗則返回false,賬戶余額不變。
值得注意的是:和本地環境相比,我們不能輕易假設分布式環境的可靠性。
所以問題來了,一種典型的情況是withdraw請求已經被服務器端正確處理,但服務器端的返回結果由于網絡等原因被掉丟了,導致客戶端無法得知處理結果。如果是在網頁上,一些不恰當的設計可能會使用戶認為上一次操作失敗了,然后刷新頁面,這就導致了withdraw被調用兩次,賬戶也被多扣了一次錢。
解決方案一:采用分布式事務
通過引入支持分布式事務的中間件來保證withdraw功能的事務性。分布式事務的優點是對于調用者很簡單,復雜性都交給了中間件來管理。
缺點則是一方面架構太重量級,容易被綁在特定的中間件上,不利于異構系統的集成
另一方面分布式事務雖然能保證事務的ACID性質,而但卻無法提供性能和可用性的保證
解決方案二:冪等設計。
我們可以通過一些技巧把withdraw變成冪等的,比如:
int create_ticket()
?
bool idempotent_withdraw(ticket_id, account_id, amount)
1
2
3
create_ticket的語義是獲取一個服務器端生成的唯一的處理號ticket_id,它將用于標識后續的操作。idempotent_withdraw和withdraw的區別在于關聯了一個ticket_id,一個ticket_id表示的操作至多只會被處理一次,每次調用都將返回第一次調用時的處理結果。這樣,idempotent_withdraw就符合冪等性了,客戶端就可以放心地多次調用。
基于冪等性的解決方案中一個完整的取錢流程被分解成了兩個步驟:
1.調用create_ticket()獲取ticket_id;
2.調用idempotent_withdraw(ticket_id, account_id, amount)。
雖然create_ticket不是冪等的,但在這種設計下,它對系統狀態的影響可以忽略,加上idempotent_withdraw是冪等的,所以任何一步由于網絡等原因失敗或超時,客戶端都可以重試,直到獲得結果。
和分布式事務相比,冪等設計的優勢在于它的輕量級,容易適應異構環境,以及性能和可用性方面。在某些性能要求比較高的應用,冪等設計往往是唯一的選擇。
2. 稍入理解
這里需要關注幾個重點:
冪等不僅僅只是一次(或多次)請求對資源沒有副作用(比如查詢數據庫操作,沒有增刪改,因此沒有對數據庫有任何影響)。
冪等還包括第一次請求的時候對資源產生了副作用,但是以后的多次請求都不會再對資源產生副作用。
冪等關注的是以后的多次請求是否對資源產生的副作用,而不關注結果。
網絡超時等問題,不是冪等的討論范圍。
冪等性是系統服務對外一種承諾(而不是實現),承諾只要調用接口成功,外部多次調用對系統的影響是一致的。聲明為冪等的服務會認為外部調用失敗是常態,并且失敗之后必然會有重試。
什么情況下需要冪等
業務開發中,經常會遇到重復提交的情況,無論是由于網絡問題無法收到請求結果而重新發起請求,
前端的操作抖動而造成重復提交情況。
在交易系統,支付系統這種重復提交造成的問題有尤其明顯,比如: 用戶在APP上連續點擊了多次提交訂單,后臺應該只產生一個訂單;
網絡抖動或者異常或者斷開導致不能正確返回
向支付寶發起支付請求,由于網絡問題或系統BUG重發,支付寶應該只扣一次錢。
很顯然,聲明冪等的服務認為,外部調用者會存在多次調用的情況,為了防止外部多次調用對系統數據狀態的發生多次改變,將服務設計成冪等。
冪等VS防重
上面例子中小明遇到的問題,只是重復提交的情況,和服務冪等的初衷是不同的。重復提交是在第一次請求已經成功的情況下,人為的進行多次操作,導致不滿足冪等要求的服務多次改變狀態。而冪等更多使用的情況是第一次請求不知道結果(比如超時)或者失敗的異常情況下,發起多次請求,目的是多次確認第一次請求成功,卻不會因多次請求而出現多次的狀態變化。
什么情況下需要保證冪等性
以SQL為例,有下面三種場景,只有第三種場景需要開發人員使用其他策略保證冪等性:
SELECT col1 FROM tab1 WHER col2=2,無論執行多少次都不會改變狀態,是天然的冪等。
UPDATE tab1 SET col1=1 WHERE col2=2,無論執行成功多少次狀態都是一致的,因此也是冪等操作。
UPDATE tab1 SET col1=col1+1 WHERE col2=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、查詢操作:
查詢一次和查詢多次,在數據不變的情況下,查詢結果是一樣的。select是天然的冪等操作;
2、刪除操作:
刪除操作也是冪等的,刪除一次和多次刪除都是把數據刪除。(注意可能返回結果不一樣,刪除的數據不存在,返回0,刪除的數據多條,返回結果多個) ;
3、唯一索引:
防止新增臟數據。比如:支付寶的資金賬戶,支付寶也有用戶賬戶,每個用戶只能有一個資金賬戶,怎么防止給用戶創建資金賬戶多個,那么給資金賬戶表中的用戶ID加唯一索引,所以一個用戶新增成功一個資金賬戶記錄。要點:唯一索引或唯一組合索引來防止新增數據存在臟數據(當表存在唯一索引,并發時新增報錯時,再查詢一次就可以了,數據應該已經存在了,返回結果即可);
4、token機制:防止頁面重復提交。
原理上通過session token來實現的(也可以通過redis來實現)。當客戶端請求頁面時,服務器會生成一個隨機數Token,并且將Token放置到session當中,然后將Token發給客戶端(一般通過構造hidden表單)。
下次客戶端提交請求時,Token會隨著表單一起提交到服務器端。
服務器端第一次驗證相同過后,會將session中的Token值更新下,若用戶重復提交,第二次的驗證判斷將失敗,因為用戶提交的表單中的Token沒變,但服務器端session中Token已經改變了。
5、悲觀鎖
獲取數據的時候加鎖獲取。select * from table_xxx where id=‘xxx’ for update; 注意:id字段一定是主鍵或者唯一索引,不然是鎖表,會死人的;悲觀鎖使用時一般伴隨事務一起使用,數據鎖定時間可能會很長,根據實際情況選用;
6、樂觀鎖
——樂觀鎖只是在更新數據那一刻鎖表,其他時間不鎖表,所以相對于悲觀鎖,效率更高。樂觀鎖的實現方式多種多樣可以通過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# >= ,這個情景適合不用版本號,只更新是做數據安全校驗,適合庫存模型,扣份額和回滾份額,性能更高;
7、分布式鎖
如果是分布式系統,構建全局唯一索引比較困難,例如唯一性的字段沒法確定,這時候可以引入分布式鎖,通過第三方的系統(redis或zookeeper),在業務系統插入數據或者更新數據,獲取分布式鎖,然后做操作,之后釋放鎖,這樣其實是把多線程并發的鎖的思路,引入多多個系統,也就是分布式系統中得解決思路。要點:某個長流程處理過程要求不能并發執行,可以在流程執行之前根據某個標志(用戶ID+后綴等)獲取分布式鎖,其他流程執行時獲取鎖就會失敗,也就是同一時間該流程只能有一個能執行成功,執行完成后,釋放分布式鎖(分布式鎖要第三方系統提供);
8、select + insert
并發不高的后臺系統,或者一些任務JOB,為了支持冪等,支持重復執行,簡單的處理方法是,先查詢下一些關鍵數據,判斷是否已經執行過,在進行業務處理,就可以了。注意:核心高并發流程不要用這種方法;
9、狀態機冪等
在設計單據相關的業務,或者是任務相關的業務,肯定會涉及到狀態機(狀態變更圖),就是業務單據上面有個狀態,狀態在不同的情況下會發生變更,一般情況下存在有限狀態機,這時候,如果狀態機已經處于下一個狀態,這時候來了一個上一個狀態的變更,理論上是不能夠變更的,這樣的話,保證了有限狀態機的冪等。注意:訂單等單據類業務,存在很長的狀態流轉,一定要深刻理解狀態機,對業務系統設計能力提高有很大幫助
10、對外提供接口的api如何保證冪等
如銀聯提供的付款接口:需要接入商戶提交付款請求時附帶:source來源,seq序列號;source+seq在數據庫里面做唯一索引,防止多次付款(并發時,只能處理一個請求) 。
重點:對外提供接口為了支持冪等調用,接口有兩個字段必須傳,一個是來源source,一個是來源方序列號seq,這個兩個字段在提供方系統里面做聯合唯一索引,這樣當第三方調用時,先在本方系統里面查詢一下,是否已經處理過,返回相應處理結果;沒有處理過,進行相應處理,返回結果。注意,為了冪等友好,一定要先查詢一下,是否處理過該筆業務,不查詢直接插入業務系統,會報錯,但實際已經處理了。、、
————————————————
版權聲明:本文為CSDN博主「OrderDong」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/fclwd/article/details/107010556
總結
以上是生活随笔為你收集整理的深入理解Java幂等性的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 深圳华侨城创意文化园核酸检测点(深圳华侨
- 下一篇: 镍铬电池充满电多少伏(镍铬电池)