轉(zhuǎn)自https://mp.weixin.qq.com/s/odRypb6YqF3xuRn-4YAwlg
https://blog.csdn.net/wanglei303707/article/details/88298211
冪等就是一個(gè)操作稠曼,不論執(zhí)行多少次虚循,產(chǎn)生的效果和返回的結(jié)果都是一樣的缸棵。
1圈澈、防止頁(yè)面重復(fù)提交——token
1.1 場(chǎng)景
頁(yè)面的數(shù)據(jù)只能被點(diǎn)擊提交一次
1.2 發(fā)生原因
由于重復(fù)點(diǎn)擊或者網(wǎng)絡(luò)重發(fā)开仰,或者nginx重發(fā)等情況會(huì)導(dǎo)致數(shù)據(jù)被重復(fù)提交
1.3場(chǎng)景:
場(chǎng)景一:在網(wǎng)絡(luò)延遲的情況下讓用戶(hù)有時(shí)間點(diǎn)擊多次submit按鈕導(dǎo)致表單重復(fù)提交
場(chǎng)景二:表單提交后用戶(hù)點(diǎn)擊【刷新】按鈕導(dǎo)致表單重復(fù)提交
場(chǎng)景三:用戶(hù)提交表單后苗傅,點(diǎn)擊瀏覽器的【后退】按鈕回退到表單頁(yè)面后進(jìn)行再次提交
1.4 解決辦法
集群環(huán)境:采用token加redis(redis單線(xiàn)程的熬甫,處理需要排隊(duì))
單JVM環(huán)境:采用token加redis或token加jvm內(nèi)存
1.5 處理流程:
1.5.1 數(shù)據(jù)提交前要向服務(wù)的申請(qǐng)token唉锌,token放到redis或jvm內(nèi)存诗舰,token有效時(shí)間警儒;
1.5.2 前端提交后,后臺(tái)校驗(yàn)token,同時(shí)刪除token蜀铲,執(zhí)行業(yè)務(wù)生成新的token返回
token特點(diǎn):要申請(qǐng)边琉,一次有效性,可以限流
因?yàn)閞edis單線(xiàn)程的原因记劝,當(dāng)多次提交時(shí)变姨,redis需要排隊(duì)處理,所以只有第一次提交可以刪除token成功厌丑,當(dāng)刪除成功代表token校驗(yàn)通過(guò)定欧,刪除失敗,說(shuō)明是重復(fù)提交怒竿。
2砍鸠、防止新增臟數(shù)據(jù)(重復(fù)數(shù)據(jù))——唯一索引
查詢(xún)操作和刪除操作天然就是冪等的:
查詢(xún)操作:查詢(xún)一次和查詢(xún)多次,在數(shù)據(jù)不變的情況下耕驰,查詢(xún)結(jié)果是一樣的爷辱。select是天然的冪等操作;
刪除操作:刪除操作也是冪等的朦肘,刪除一次和多次刪除都是把數(shù)據(jù)刪除饭弓。(注意可能返回結(jié)果不一樣,刪除的數(shù)據(jù)不存在厚骗,返回0示启,刪除的數(shù)據(jù)多條兢哭,返回結(jié)果多個(gè))
新增操作:有些服務(wù)是有重試機(jī)制的(ribbon)领舰,在這種情況下,就可能會(huì)出現(xiàn)新增兩條一樣的數(shù)據(jù)迟螺,這時(shí)候服務(wù)要支持冪等操作冲秽,否則會(huì)出問(wèn)題,這時(shí)候可以通過(guò)數(shù)據(jù)庫(kù)唯一索引來(lái)防止新增臟數(shù)據(jù)矩父。
比如:支付寶的資金賬戶(hù)锉桑,支付寶也有用戶(hù)賬戶(hù),每個(gè)用戶(hù)只能有一個(gè)資金賬戶(hù)窍株,怎么防止給用戶(hù)創(chuàng)建資金賬戶(hù)多個(gè)民轴,那么給資金賬戶(hù)表中的用戶(hù)ID加唯一索引,所以一個(gè)用戶(hù)新增成功一個(gè)資金賬戶(hù)記錄
2.1 唯一索引或唯一組合索引來(lái)防止新增數(shù)據(jù)存在臟數(shù)據(jù)
當(dāng)表存在唯一索引球订,并發(fā)時(shí)新增報(bào)錯(cuò)時(shí)后裸,再查詢(xún)一次就可以了,數(shù)據(jù)應(yīng)該已經(jīng)存在了冒滩,返回結(jié)果即可
2.2 防重表
使用訂單號(hào)orderNo做為去重表的唯一索引微驶,每次請(qǐng)求都根據(jù)訂單號(hào)向去重表中插入一條數(shù)據(jù)。第一次請(qǐng)求查詢(xún)訂單支付狀態(tài),當(dāng)然訂單沒(méi)有支付因苹,進(jìn)行支付操作苟耻,無(wú)論成功與否,執(zhí)行完后更新訂單狀態(tài)為成功或失敗扶檐,刪除去重表中的數(shù)據(jù)凶杖。后續(xù)的訂單因?yàn)楸碇形ㄒ凰饕迦胧。瑒t返回操作失敗款筑,直到第一次的請(qǐng)求完成(成功或失敼倏ā)〈茁玻可以看出防重表作用是加鎖的功能寻咒。
防重表可以在一些主業(yè)務(wù)表不方便加唯一索引或者唯一組合索引時(shí)其作用,通過(guò)單獨(dú)新建一張防重表來(lái)解決問(wèn)題
3颈嚼、對(duì)外提供接口的api如何保證冪等
如銀聯(lián)提供的付款接口:需要接入商戶(hù)提交付款請(qǐng)求時(shí)附帶:source來(lái)源毛秘,seq序列號(hào),source+seq在數(shù)據(jù)庫(kù)里面做唯一索引阻课,防止多次付款叫挟,(并發(fā)時(shí),只能處理一個(gè)請(qǐng)求)
重點(diǎn): 對(duì)外提供接口為了支持冪等調(diào)用限煞,接口有兩個(gè)字段必須傳抹恳,一個(gè)是來(lái)源source,一個(gè)是來(lái)源方序列號(hào)seq署驻,這個(gè)兩個(gè)字段在提供方系統(tǒng)里面做聯(lián)合唯一索引
這樣當(dāng)?shù)谌秸{(diào)用時(shí)奋献,先在本方系統(tǒng)里面查詢(xún)一下,是否已經(jīng)處理過(guò)旺上,返回相應(yīng)處理結(jié)果瓶蚂;沒(méi)有處理過(guò),進(jìn)行相應(yīng)處理宣吱,返回結(jié)果窃这。
注意,為了冪等友好征候,一定要先查詢(xún)一下杭攻,是否處理過(guò)該筆業(yè)務(wù),不查詢(xún)直接插入業(yè)務(wù)系統(tǒng)疤坝,會(huì)報(bào)錯(cuò)兆解,但實(shí)際已經(jīng)處理了。
這里使用的還是數(shù)據(jù)庫(kù)唯一索引(聯(lián)合唯一索引)卒煞。
對(duì)于一些有時(shí)效性的業(yè)務(wù)處理接口來(lái)說(shuō)痪宰,事實(shí)上還有一種方法,就是使用redis。
- 當(dāng)?shù)谌秸{(diào)用時(shí)衣撬,先判斷是否過(guò)期乖订,如果過(guò)期了,就不處理具练,如果沒(méi)有過(guò)期乍构,則下一步;
- 判斷redis中是否存在source_seq key扛点,如果存在哥遮,說(shuō)明已經(jīng)處理過(guò)了,是重復(fù)調(diào)用陵究,直接返回眠饮;如果不存在,則set source_seq key 铜邮,注意這里的存在則返回仪召,不存在則set key,是需要保持原子操作的松蒜,可以通過(guò)lua腳本來(lái)提交扔茅;
- key有效期和業(yè)務(wù)有關(guān)系, 比如說(shuō)我支付消息有效期只有1h秸苗,那么key 有效期也是1h召娜;
4、同一時(shí)間只能完成一次請(qǐng)求——樂(lè)觀鎖惊楼、分布式鎖
在分布式環(huán)境下玖瘸,因?yàn)榫W(wǎng)絡(luò)、重試等原因?qū)е乱粋€(gè)長(zhǎng)流程請(qǐng)求經(jīng)過(guò)不同的實(shí)例時(shí)胁后,會(huì)發(fā)生重復(fù)操作店读,為了防止這種情況,可以采用樂(lè)觀鎖攀芯,或者分布式鎖來(lái)解決。
使用樂(lè)觀鎖文虏,或者分布式鎖侣诺,其實(shí)就是在mysql中,同一時(shí)間只有一次請(qǐng)求氧秘。
在有些場(chǎng)景下年鸳,使用version樂(lè)觀鎖還是麻煩,其實(shí)可以使用主鍵id丸相,或者唯一索引來(lái)鎖定mysql數(shù)據(jù)搔确,然后再操作,比如狀態(tài)變更,如下
update tableA set status = 5 where id=4 and status=4;
就算同一時(shí)間有重復(fù)操作膳算,但是因?yàn)閙ysql行鎖的特性座硕,同一時(shí)間只有一個(gè)操作能鎖定id為4的這行數(shù)據(jù),狀態(tài)變更成功以后(返回值為1)涕蜂,后面的操作已經(jīng)不滿(mǎn)足status=4的條件了华匾,利用這個(gè)特性,不止能防重復(fù)机隙,對(duì)于一些并發(fā)開(kāi)獎(jiǎng)場(chǎng)景蜘拉,可以直接替代樂(lè)觀鎖的使用。
5有鹿、總結(jié)
查詢(xún)操作:select旭旭,查詢(xún)一次和查詢(xún)多次,在數(shù)據(jù)不變的情況下葱跋,查詢(xún)結(jié)果是一樣的您机。select是天然的冪等操作;
刪除操作:delete年局,刪除操作也是冪等的际看,刪除一次和多次刪除都是把數(shù)據(jù)刪除。(注意可能返回結(jié)果不一樣矢否,刪除的數(shù)據(jù)不存在仲闽,返回0,刪除的數(shù)據(jù)多條僵朗,返回結(jié)果多個(gè))赖欣;
新增操作:insert,業(yè)務(wù)邏輯上验庙,先select再insert示血;數(shù)據(jù)庫(kù)上盡量通過(guò)唯一索引或唯一組合索引來(lái)防止新增臟數(shù)據(jù)(重復(fù)數(shù)據(jù))喇肋;如果沒(méi)有唯一索引,則再考慮分布式鎖。
更新操作:update拢切,對(duì)于狀態(tài)操作,where條件中魏滚,一定要加上原本的狀態(tài)值扫倡,即明確是由舊狀態(tài)變更為新?tīng)顟B(tài),這個(gè)過(guò)程是不可逆的藤巢。
如果上面的一般方法還是不能防重搞莺,則可以使用redis分布式鎖。
使用token+redis的好處是掂咒,一個(gè)token只能使用一次才沧,token使用或者過(guò)期后迈喉,就只能再次申請(qǐng)token,是嚴(yán)格防止重復(fù)的温圆。但是會(huì)比較麻煩挨摸,一個(gè)請(qǐng)求,需要兩次操作捌木,一次獲取token油坝,一次執(zhí)行業(yè)務(wù)。
其實(shí)可以簡(jiǎn)單的只用redis分布式鎖來(lái)防止重復(fù)刨裆,把獲取鎖和釋放鎖的邏輯寫(xiě)在過(guò)濾器澈圈,或者攔截器中,只有獲取到鎖帆啃,才能執(zhí)行業(yè)務(wù)邏輯瞬女。—— 對(duì)那些需要加防重的接口url努潘,可以放在配置文件中诽偷,而不要放在redis中,因?yàn)榻^對(duì)部分接口url是不需要防重的疯坤,如果放在redis中报慕,每個(gè)請(qǐng)求都會(huì)去redis中獲取url配置,太浪費(fèi)redis了压怠,如果趕上公司搞活動(dòng)眠冈,很容易就報(bào)警了。
key的設(shè)置:可以先把請(qǐng)求參數(shù)中為空的去掉菌瘫,然后拼接為String蜗顽,并對(duì)其MD5加密,再加上userId雨让,最后拼接成key雇盖。
至于redis分布式的實(shí)現(xiàn),可以見(jiàn)redis系列文章栖忠。http://www.reibang.com/nb/39445385