1、什么是冪等性
冪等彻况,英文Idempotence
冪等這個詞原自數(shù)學(xué)谁尸,冪等性是數(shù)學(xué)中的一個概念,常見于抽象代數(shù)中纽甘,表達(dá)的是N次變換與1次變換的結(jié)果相同良蛮;
簡單來說就是如果方法調(diào)用一次和多次產(chǎn)生的效果是相同的,它就具有冪等性悍赢。
冪等函數(shù)或冪等方法决瞳,是指可以使用相同參數(shù)重復(fù)執(zhí)行,并能獲得相同結(jié)果的函數(shù)泽裳,這些函數(shù)不會影響系統(tǒng)狀態(tài)瞒斩,也不用擔(dān)心重復(fù)執(zhí)行會對系統(tǒng)造成改變。
冪等性(Idempotence)本身是一個數(shù)學(xué)概念涮总,在計(jì)算機(jī)的各個領(lǐng)域都借用了該概念胸囱。
HTTP維度的冪等性
在HTTP/1.1規(guī)范中冪等性的定義是:
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.
從定義上看,HTTP方法的冪等性是指一次和多次請求某一個資源應(yīng)該具有同樣的副作用瀑梗。
HTTP請求常見有?GET烹笔、DELETE、PUT抛丽、POST四種主要方法谤职;
GET方法
HTTP GET方法用于獲取資源,不應(yīng)有副作用亿鲜,所以是冪等的允蜈。
比如:GEThttps://www.wkcto.com/course/100?不會改變資源的狀態(tài),不論調(diào)用一次還是N次都沒有副作用蒿柳。
請注意饶套,這里強(qiáng)調(diào)的是一次和N次具有相同的副作用,而不是每次GET的結(jié)果相同垒探。
GET https://www.wkcto.com/course?這個HTTP請求可能會每次得到不同的結(jié)果妓蛮,但它本身并沒有產(chǎn)生任何副作用,因而是滿足冪等性的圾叼。
DELETE方法
HTTP DELETE方法用于刪除資源蛤克,有副作用捺癞,但它應(yīng)該滿足冪等性。
比如:DELETEhttps://www.wkcto.com/article/detail/10?构挤,調(diào)用一次和N次對系統(tǒng)產(chǎn)生的副作用是相同的髓介,即刪掉id為10的文章,因此調(diào)用者可以多次調(diào)用或刷新頁面而不必?fù)?dān)心引起錯誤儿倒。
POST方法
HTTP POST所對應(yīng)的URI為資源的接收者版保。
比如:
POST https://www.wkcto.com/article?的語義是在https://www.wkcto.com/article下發(fā)表一篇文章,兩次相同的POST請求會在服務(wù)器端創(chuàng)建兩份資源夫否,所以POST方法不具備冪等性彻犁。
PUT方法
HTTP PUT所對應(yīng)的URI是要創(chuàng)建或更新的資源。
比如:PUThttps://www.wkcto.com/article/5231?的語義是創(chuàng)建或更新ID為5231的文章凰慈,對同一URI進(jìn)行多次PUT的副作用和一次PUT是相同的汞幢,因此PUT方法具有冪等性。
以上是主要針對RESTful風(fēng)格的HTTP冪等性討論微谓;
我們知道HTTP協(xié)議是一種面向資源的應(yīng)用層協(xié)議森篷,但對HTTP協(xié)議的應(yīng)用存在兩種不同的方式:
一種是RESTful的,它把HTTP當(dāng)成應(yīng)用層協(xié)議豺型,遵守HTTP協(xié)議的各種規(guī)定仲智;
另一種是在HTTP協(xié)議之上封裝的RPC,沒有完全把HTTP當(dāng)成應(yīng)用層協(xié)議姻氨,而是把HTTP協(xié)議作為了傳輸層協(xié)議钓辆,然后在HTTP之上建立了自己的應(yīng)用層協(xié)議。
那么拋開HTTP協(xié)議的規(guī)范肴焊,冪等性是分布式系統(tǒng)的重要特性前联,所以不論是RESTful的API設(shè)計(jì)還是RPC方式的其他API設(shè)計(jì)都應(yīng)該考慮冪等性;
應(yīng)用維度的冪等性
冪等性衍生到軟件工程中, 它的語義是指函數(shù)/接口可以使用相同的參數(shù)重復(fù)執(zhí)行, 不應(yīng)該影響系統(tǒng)狀態(tài), 也不會對系統(tǒng)造成改變娶眷。
也就是任意多次執(zhí)行所產(chǎn)生的影響均與一次執(zhí)行所產(chǎn)生的影響相同似嗤;
如果用戶對同一操作發(fā)起的一次請求或多次請求所產(chǎn)生的影響是一致的,不會因?yàn)槎啻握{(diào)用(點(diǎn)擊)而產(chǎn)生了副作用届宠,那么這就是冪等的烁落;
第一次請求的時候?qū)Y源產(chǎn)生了副作用,但是以后的多次請求都不會再對資源產(chǎn)生副作用豌注。這里的副作用是指不會對結(jié)果產(chǎn)生破壞或者產(chǎn)生不可預(yù)料的結(jié)果伤塌。
即冪等性=多次執(zhí)行無副作用;
2幌羞、產(chǎn)生冪等性場景
冪等性問題在我們的開發(fā)中寸谜,分布式竟稳、微服務(wù)架構(gòu)中是隨處可見的:
1属桦、因網(wǎng)絡(luò)波動熊痴,可能會引起重復(fù)請求;
2聂宾、用戶重復(fù)操作果善,用戶在使用產(chǎn)品時可能會無意的觸發(fā)多次下單多次交易,甚至沒有響應(yīng)而有意觸發(fā)多筆交易系谐;
3巾陕、應(yīng)用使用了失敗或超時重試機(jī)制(如Nginx重試、RPC重試或業(yè)務(wù)層重試等)
4纪他、第三方平臺的接口(如:支付成功回調(diào)接口)鄙煤,因?yàn)楫惓?dǎo)致多次異步回調(diào);
5茶袒、中間件/應(yīng)用服務(wù)根據(jù)自身的特性梯刚,也有可能進(jìn)行重試。
6薪寓、用戶雙擊提交按鈕;
7亡资、頁面重復(fù)刷新;
8、使用瀏覽器后退按鈕重復(fù)之前的操作向叉,導(dǎo)致重復(fù)提交表單;
9锥腻、使用瀏覽器歷史記錄重復(fù)提交表單;
10、瀏覽器重復(fù)的HTTP請求母谎;
11瘦黑、定時任務(wù)重復(fù)執(zhí)行;
3销睁、冪等在哪一層實(shí)現(xiàn)
我們現(xiàn)在都是分布式供璧、微服務(wù)的架構(gòu),在哪一層進(jìn)進(jìn)行冪等設(shè)計(jì)冻记,在哪一層解決冪等性問題睡毒?
4、數(shù)據(jù)訪問層的冪等性
讀請求
寫請求
讀請求需要做冪等嗎冗栗?很顯然是不需要的演顾;
寫請求呢?涉及到需要做insert隅居、update钠至、delete數(shù)據(jù)庫操作的,肯定是需要的實(shí)現(xiàn)冪等性的胎源;
那我們可以得出一個結(jié)論棉钧,即不會改變數(shù)據(jù)的操作我們可以不做冪等,會改變數(shù)據(jù)的操作我們就一定要做冪等涕蚤;
那我們逐個討論寫請求:insert宪卿、delete的诵、update操作,首先我假設(shè)我沒有做任何應(yīng)用層面上的冪等操作佑钾。
insert
對于insert操作西疤,當(dāng)我重復(fù)插入數(shù)據(jù)的時候會出現(xiàn)什么情況?這里分兩種情況:
自增主鍵(有冪等性問題)
業(yè)務(wù)主鍵(沒有冪等性問題)
比如:insert into product_info(id, name, type, price, tm)休溶;
假如我的id是自增主鍵會有問題嗎代赁?一定會有冪等性問題,因?yàn)闀a(chǎn)生多條業(yè)務(wù)數(shù)據(jù)相同但主鍵不同的數(shù)據(jù)兽掰。
那如果是業(yè)務(wù)主鍵呢芭碍?即我假設(shè)對name、type孽尽、price建立唯一索引豁跑,這樣就ok了,即使我id相同泻云,數(shù)據(jù)庫也會報錯了艇拍。
delete
對于delete操作,當(dāng)重復(fù)執(zhí)行的時候會出現(xiàn)什么情況宠纯?這里也要分兩種情況:
相對值刪除
絕對值刪除
比如:
delete from product_info where id = 1234; --冪等的
delete top(10) from product_info; --不是冪等
如果是絕對值刪除卸夕,重復(fù)操作兩次是不會出現(xiàn)問題的,但是如果相對值刪除,重復(fù)操作就是重復(fù)刪除多次婆瓜。
update
對于update操作快集,當(dāng)重復(fù)更新數(shù)據(jù)的時候會出現(xiàn)什么情況?這里其實(shí)和刪除操作是一樣廉白,也需要分兩種情況討論:
相對值刪除
絕對值刪除
我們拿一個具體的例子分析:
update product_info set price = 99 where id = 1234; ?--冪等的
update product_info set price = price + 100 where id = 1234; --不是冪等
如果是絕對值修改个初,重復(fù)操作也不會有問題,但是相對值修改猴蹂,一定會有問題院溺,會重復(fù)修改多次。
select
最后是select操作磅轻,其實(shí)這個不用討論珍逸,因?yàn)椴粫?shù)據(jù)發(fā)生改變的操作我們不用做冪等。
狹義與廣義的冪等
以上的所有討論都是基于單庫的聋溜,這是狹義上的冪等處理谆膳,但是在實(shí)際的業(yè)務(wù)場景中,比如分布式系統(tǒng)中撮躁,我們的一次請求可能有多個步驟漱病,這種跨服務(wù)、跨事務(wù)請求的冪等處理怎么辦?也就是廣義上的冪等處理怎么辦呢杨帽?其實(shí)這個就需要分布式事務(wù)來保證冪等性凝果;
所以廣義上的冪等處理通過分布式事務(wù)來解決,狹義上的冪等處理睦尽,對于服務(wù)分層來說只需要在數(shù)據(jù)訪問層做冪等操作,而對于讀寫請求冪等處理型雳,select我們不用處理当凡,insert操作只要要求必須有唯一業(yè)務(wù)主鍵,delete操作在實(shí)際業(yè)務(wù)上一般不會被允許纠俭,update操作只需要把相對值修改轉(zhuǎn)換成絕對值修改即可沿量。
5、保證冪等性的方法
前端冪等性的實(shí)現(xiàn)(不是可靠的)
1冤荆、按鈕只可操作一次
一般是提交后把按鈕置灰或loding狀態(tài)朴则,按鈕置灰或loding狀態(tài)可以用一些js組件實(shí)現(xiàn),消除用戶因?yàn)橹貜?fù)點(diǎn)擊而產(chǎn)生的副作用钓简,比如添加操作乌妒,由于點(diǎn)擊兩次而產(chǎn)生兩條記錄。
2外邓、token機(jī)制
產(chǎn)品上允許重復(fù)提交撤蚊,但要保證重復(fù)提交不產(chǎn)生副作用,比如點(diǎn)擊n次只產(chǎn)生一條記錄损话;
具體實(shí)現(xiàn)就是進(jìn)入頁面時申請一個token侦啸,然后后面所有的請求都帶上這個token,根據(jù)token來避免重復(fù)請求丧枪;
3光涂、使用Post/Redirect/Get模式
在提交后執(zhí)行頁面重定向,這就是所謂的Post-Redirect-Get (PRG)模式拧烦。簡言之忘闻,當(dāng)用戶提交了表單后,去執(zhí)行一個客戶端的重定向恋博,轉(zhuǎn)到提交成功信息頁面服赎,這樣避免用戶按F5刷新導(dǎo)致的重復(fù)提交,而其也不會出現(xiàn)瀏覽器表單重復(fù)提交的警告交播,也能消除按瀏覽器前進(jìn)和后退按導(dǎo)致的同樣重復(fù)提交的問題重虑;
4、在Session中存放特殊標(biāo)志
在服務(wù)器端秦士,生成一個唯一的標(biāo)識符缺厉,將它存入session,同時將它寫入表單的隱藏中,然后將表單頁面發(fā)給瀏覽器提针,用戶輸入信息后點(diǎn)擊提交命爬,在服務(wù)器端,獲取表單中隱藏字段的值辐脖,與session中的唯一標(biāo)識符比較饲宛,相等說明是首次提交,就處理本次請求嗜价,然后將session中的唯一標(biāo)識符移除艇抠,不相等則表示是重復(fù)提交,不再做處理久锥;
后端冪等性的實(shí)現(xiàn)
1家淤、使用唯一索引防止冪等性問題
此方案可以限制重復(fù)插入數(shù)據(jù),當(dāng)數(shù)據(jù)重復(fù)時瑟由,插入數(shù)據(jù)庫會拋異常絮重,保證不會出現(xiàn)臟數(shù)據(jù),這也是一種簡單粗暴的辦法歹苦;
2青伤、Token+Redis的冪等方案
這種方式分成兩個階段:申請token階段和業(yè)務(wù)操作階段。
以支付為例:
第一階段殴瘦,在進(jìn)入到提交訂單頁面之前潮模,需要訂單系統(tǒng)根據(jù)用戶信息向支付系統(tǒng)發(fā)起一次申請token的請求,支付系統(tǒng)將token保存到Redis緩存中痴施,為第二階段支付使用擎厢。
第二階段,訂單系統(tǒng)拿著申請到的token發(fā)起支付請求辣吃,支付系統(tǒng)會檢查Redis中是否存在該token动遭,如果存在,表示第一次發(fā)起支付請求神得,開始支付邏輯處理厘惦,處理完邏輯后刪除redis中的token;
當(dāng)重復(fù)請求時候哩簿,檢查緩存中token不存在宵蕉,表示非法請求。
該方案的不足之處是需要與系統(tǒng)間交互兩次节榜;
3羡玛、狀態(tài)機(jī)冪等
針對更新操作,比如業(yè)務(wù)上需要修改訂單狀態(tài)宗苍,訂單有待支付稼稿、支付中薄榛、支付成功、支付失敗让歼、訂單超時關(guān)閉等敞恋,在設(shè)計(jì)的時候最好只支持狀態(tài)的單向改變(不可逆),這樣在更新的時候where條件里可以加上status?= 我期望的原來的status谋右,多次調(diào)用的話實(shí)際上也只會執(zhí)行一次硬猫。
Update xx set status=‘支付中’?where status=’待支付’and id=xx;
4改执、樂觀鎖實(shí)現(xiàn)冪等
如果更新已有數(shù)據(jù)啸蜜,可以進(jìn)行加鎖更新,也可以設(shè)計(jì)表結(jié)構(gòu)時使用樂觀鎖天梧,通過version來做樂觀鎖,這樣既能保證執(zhí)行效率霞丧,又能保證冪等呢岗。樂觀鎖的version版本在更新業(yè)務(wù)數(shù)據(jù)要自增。
1蛹尝、查詢數(shù)據(jù)后豫,得到版本號;version=1
2突那、通過版本號去更新挫酿,版本號匹配就更新,版本號不匹配就不能更新愕难;update xxx set money = money - 99, version = version + 1 ?where id = xx and version = 1;
也可以采用update with condition早龟,更新帶條件,實(shí)現(xiàn)樂觀鎖猫缭,通過version或者其他條件來實(shí)現(xiàn)樂觀鎖葱弟;
update table_xxx set quality=quality-#subQuality#,version=version+1 where id=xx and version=#version#
帶條件的樂觀鎖:
update table_xxx set quality=quality-#subQuality# where quality-#subQuality# >= 0
5、防重表實(shí)現(xiàn)冪等性
需要增加一個表猜丹,這個表叫做防重表(防止數(shù)據(jù)重復(fù)的表)
使用唯一主鍵去做防重表的唯一索引芝加,比如使用訂單號orderNo做為防重表的唯一索引,每次請求都根據(jù)訂單號向去重表中插入一條數(shù)據(jù)射窒,第一次請求查詢訂單支付狀態(tài)藏杖,當(dāng)然訂單沒有支付,進(jìn)行支付操作脉顿,支付前先向防重表中插入該支付的訂單號蝌麸,插入成功說明可以支付,無論成功與否艾疟,執(zhí)行完后更新訂單狀態(tài)為成功或其他狀態(tài)祥楣,或者是失敗开财,然后可以刪除防重表中的數(shù)據(jù)。后續(xù)的訂單因?yàn)楸碇形ㄒ凰饕迦胧∥笸剩瑒t返回操作失敗责鳍,直到第一次的請求操作完成(成功或失敗)兽间,可以看出防重表作用是加鎖的功能历葛;
select + insert
該方案就是操作之前先查詢一下,符合要求再插入嘀略,該方案在沒有并發(fā)的系統(tǒng)中可以解決冪等問題恤溶,在單JVM有并發(fā)的時候可以JVM加鎖來保證冪等性,在分布式環(huán)境它是無法保證冪等性帜羊,可以使用分布式鎖來保證咒程。
6、分布式鎖保證冪等性
在進(jìn)入方法時讼育,先去獲取鎖帐姻,假如獲取到鎖,就繼續(xù)后面的流程奶段。假如沒有獲取到鎖饥瓷,就等待鎖的釋放直到獲取到鎖,當(dāng)執(zhí)行完方法時痹籍,釋放鎖呢铆,當(dāng)然,鎖要設(shè)個超時時間蹲缠,防止意外沒有釋放到鎖棺克,它可以用來解決分布式系統(tǒng)的冪等性;
常用的分布式鎖實(shí)現(xiàn)方案是redis 和 zookeeper 等工具线定。
使用分布式鎖類似于防重表逆航,將防重并發(fā)放到了緩存中,較為高效渔肩。思路相同因俐,同一時間只能完成一次支付請求。
7周偎、緩沖隊(duì)列
將請求都快速地接收下來抹剩,放入緩沖隊(duì)列,后續(xù)使用異步任務(wù)處理隊(duì)列中的數(shù)據(jù)蓉坎,過濾掉重復(fù)的請求澳眷,此方案優(yōu)點(diǎn)是同步改為異步處理,高吞吐蛉艾,不足是不能及時地返回請求結(jié)果钳踊,需要后續(xù)輪詢處理結(jié)果衷敌。
8、全局唯一號實(shí)現(xiàn)冪等
比如通過source來源+seq序列號來判斷請求是否重復(fù)拓瞪,在并發(fā)時只能處理一個請求缴罗,其它相同并發(fā)請求要么返回請求重復(fù),要么等待前面請求執(zhí)行完成在執(zhí)行祭埂;
小結(jié)
冪等性雖然復(fù)雜化了業(yè)務(wù)功能和降低了執(zhí)行效率面氓,但為了保證系統(tǒng)的正確性,是必要的蛆橡。保證方法或接口的冪等性是非常有必要的舌界,因?yàn)閿?shù)據(jù)是不能出現(xiàn)任何問題的;