java中接口幂等性解决方案总结
一、概念
一個冪等操作的特點是其任意多次執行所產生的影響均與一次執行的影響相同。冪等函數,或冪等方法,是指可以使用相同參數重復執行,并能獲得相同結果的函數。這些函數不會影響系統狀態,也不用擔心重復執行會對系統造成改變。
二、場景
1、前端頁面在填寫一些表單點擊提交保存按鈕的時候,因網絡波動沒有及時對用戶做出提交成功響應,致使用戶認為沒有成功提交,然后一直點提交按鈕,這時就會發生重復提交表單請求,后端收到了好幾次提交,這時就會在數據庫中重復創建了多條記錄,這就是接口沒有冪等性帶來的 bug。
2、接口惡意調用刷單,比如投票功能,針對某一個用戶重復提交,會導致接口接收到用戶重復提交的投票信息,這樣會使投票結果與事實嚴重不符。
3、 一個訂單創建接口,第一次調用超時了,然后調用方重試了一次,雖然第一次超時了,但是實際也許創建成功了,再次調用接口重試,這個時候就會調用2次創建接口,會創建2張訂單,實際我們只想創建一張訂單。
4、電商系統訂單消耗庫存場景:在訂單創建時,我們需要去扣減庫存,由于種種原因接口發生了超時,調用方重試了一次,如果接口不是冪等的,就有可能減2次庫存。我們重試的目的,其實只是想一次成功的請求,如果真的減去2次庫存,那就不滿足需求。
5、電商系統訂單退款的場景:當用戶發起退款,退款接口超時,長時間未返回是否退款成功的結果,退款接口調用方重試一次,結果2次的退款請求都成功了,則會給用戶退2次錢。
6、使用消息中間件來處理消息隊列,且手動 ack 確認消息被正常消費時。如果消費者突然斷開連接,那么已經執行了一半的消息會重新放回隊列。當消息被其他消費者重新消費時,如果沒有冪等性,就會導致消息重復消費時結果異常,如數據庫重復數據,數據庫數據沖突,資源重復等。
…
三、解決方案
1、數據庫唯一標識
在數據庫唯一主鍵或者在相關的字段上添加唯一索引,客戶端執行創建請求,調用服務端接口,后端生成布式 ID(雪花id、redis生成全局id等等方法) 充當主鍵或者建立唯一索引的字段值,這樣才能能保證在分布式環境下 ID 的全局唯一性,后端將該條數據插入數據庫中,如果插入成功則表示沒有重復調用接口。如果拋出主鍵重復異常,則表示數據庫中已經存在該條記錄,返回錯誤信息到客戶端;
2、樂觀鎖
建表test01,在test01表中添加一個標識字段version,初始值設為1;
現在需要將張繼科的age + 10;
更新前先查詢張繼科當前的version:1;
更新數據的同時version+1,條件加上version = 1 (當前線程查到的版本號),然后判斷本次update操作的影響行數,如果大于0,則說明本次更新成功,如果等于0,則說明本次更新沒有讓數據變更。
由于第一次請求version等于1是可以成功的,操作成功后version變成2了。這時如果并發的請求過來,再執行相同的sql:
update test01 set age = age + 100,version = version + 1 where name = '張繼科' and version = 1;該update操作不會真正更新數據,最終sql的執行結果影響行數是0,因為version已經變成2了,where中的version = 1肯定無法滿足條件。但為了保證接口冪等性,接口可以直接返回成功,因為version值已經修改了,那么前面必定已經成功過一次,后面都是重復的請求。
注意:樂觀鎖的更新操作,最好用主鍵或者唯一索引來更新,這樣是行鎖,否則更新時會鎖表;索引與表鎖行鎖的問題,我其他文章有詳細說明;
3、悲觀鎖
要使用悲觀鎖,我們必須關閉mysql數據庫的自動提交屬性,因為MySQL默認使用autocommit模式,當你執行一個更新操作后,MySQL會立刻將結果進行提交。
查詢是否開啟自動提交事務
select @@autocommit;設置為手動提交事務
set autocommit = 0;假如實際業務中:需要將張繼科的age修改為16;
開啟事務:
查詢并鎖當前行;注意:name字段上有索引;
select * from test01 where name = '張繼科' for UPDATE;執行業務,張繼科的年齡改為16
UPDATE test01 set age = 16 where name = '張繼科';注意:暫時先不執行: commint
模擬另一個線程(新建一個查詢窗口):
#執行業務,張繼科的年齡改為32
UPDATE test01 set age = 32 where name = ‘張繼科’;
它會一直處于阻塞狀態;
直到剛才修改age為16的線程提交(commint),才會釋放;其他線程才能更新操作;不影響查詢操作;我們現在把剛才修改age為16的線程commint:更新成功;其他線程可以正常更新;
需要特別注意的是:如果使用的是mysql數據庫,存儲引擎必須用innodb,因為它才支持事務。此外,這里name字段一定要建立索引,不然會鎖住整張表。悲觀鎖需要在同一個事務操作過程中鎖住一行數據,如果事務耗時比較長,會造成大量的請求等待,影響接口性能。此外,每次請求接口很難保證都有相同的返回值,所以不適合冪等性設計場景,但是在防重場景中是可以的使用的。在這里順便說一下,防重設計 和 冪等設計,其實是有區別的。防重設計主要為了避免產生重復數據,對接口返回沒有太多要求。而冪等設計除了避免產生重復數據之外,還要求每次請求都返回一樣的結果。
4.Token機制
客戶端在調用接口的時候向后臺請求一個全局id(token),請求的時候就攜帶這個全局id傳到后臺,后端對這個token作為key,用戶信息(sessionId)作為value,以鍵值對的方式在redis中進行校驗,如果key相同且value匹配則刪除,然后執行刪除操作(存的是需要設置失效時間,刪除時候注意原子性操作),否則屬于重復提交;
a、服務端提供生成token的接口,注意全局唯一;
b、客戶端調用接口獲取token,同時后端將token放到redis中,token作為key,用戶信息為value;
c、客戶端將獲取到的token放到當前表單隱藏域中;
d、客戶端在執行提交表單時,把 token 存入到 Headers 中,執行業務請求帶上該 Headers;
e、服務端收到請求后,從header中拿到token,根據key在redis中查找是否存在;
f、服務端根據 Redis 中是否存該 key 進行判斷,如果存在就將該 key 刪除,然后正常執行業務邏輯。如果不存在就拋異常,返回重復提交的錯誤信息。
注意:在并發情況下,執行 Redis 查找數據與刪除需要保證原子性,否則很可能在并發下無法保證冪等性。其實現方法可以使用分布式鎖或者使用 Lua 表達式來注銷查詢與刪除操作
5.分布式鎖
在業務系統插入數據或者更新數據,先獲取鎖,獲取到鎖,就繼續后面的業務邏輯。如果沒有獲取到鎖,就等待鎖的釋放直到獲取鎖,當執行完業務邏輯時,釋放鎖,當然,鎖要設置超時時間,防止意外沒有釋放到鎖,它可以用來解決分布式系統的冪等性,布式鎖類似于防重表,將防重并發放到了緩存中,較為高效,同一時間只能完成一次操作,常用的分布式鎖實現方案是redis和zookeeper等;目前redision是最常用的,它不需要我們過于考慮原子操作,它包含了常用鎖的類型,基本的可重入鎖,讀寫鎖,以及CountDownLatch的設置及使用,redisson的作者就是在加鎖和解鎖的執行層面采用Lua腳本,有原子性保證;總之它很輕大,我會在后面總結關于分布式鎖的詳細內容。
四、總結
冪等性應該是合格程序員的一個基因,在設計系統時,是首要考慮的問題,尤其是在像支付寶,銀行,互聯網金融公司等涉及的都是錢的系統,既要高效,數據也要準確,所以不能出現多扣款,多打款等問題,這樣會很難處理,用戶體驗也不好。另外,冪等性是為了簡化客戶端邏輯處理,能放置重復提交等操作,但卻增加了服務端的邏輯復雜性和成本,其主要是:并行執行的功能改為串行執行,降低了執行效率。增加了額外控制冪等的業務邏輯,復雜化了業務功能;所以在使用時候需要考慮是否引入冪等性的必要性,根據實際業務場景具體分析,除了業務上的特殊要求外,一般情況下不需要引入的接口冪等性,
總結
以上是生活随笔為你收集整理的java中接口幂等性解决方案总结的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: GraphicsView学习-基本图元使
- 下一篇: 修炼内功