淺談Redis處理接口冪等性的兩種方案
前言:接口冪等性
問題,對(duì)于開發(fā)人員來說,是一個(gè)跟語(yǔ)言無關(guān)的公共問題。對(duì)于一些用戶請(qǐng)求,在某些情況下是可能重復(fù)發(fā)送的,如果是查詢類操作并無大礙,但其中有些是涉及寫入操作的,一旦重復(fù)了,可能會(huì)導(dǎo)致很嚴(yán)重的后果,例如交易的接口如果重復(fù)請(qǐng)求可能會(huì)重復(fù)下單。接口冪等性是指用戶對(duì)于同一操作發(fā)起的一次請(qǐng)求或者多次請(qǐng)求的結(jié)果是一致的,不會(huì)因?yàn)槎啻吸c(diǎn)擊而產(chǎn)生了副作用。
一、接口冪等性
1.1、什么是接口冪等性
在HTTP/1.1中,對(duì)冪等性進(jìn)行了定義。它描述了一次和多次請(qǐng)求某一個(gè)資源對(duì)于資源本身應(yīng)該具有同樣的結(jié)果,即第一次請(qǐng)求的時(shí)候?qū)Y源產(chǎn)生了副作用,但是以后的多次請(qǐng)求都不會(huì)再對(duì)資源產(chǎn)生副作用。這里的副作用是不會(huì)對(duì)結(jié)果產(chǎn)生破壞或者產(chǎn)生不可預(yù)料的結(jié)果。也就是說,其任意多次執(zhí)行對(duì)資源本身所產(chǎn)生的影響均與一次執(zhí)行的影響相同。
這類問題多發(fā)于接口的:
insert
操作,這種情況下多次請(qǐng)求,可能會(huì)產(chǎn)生重復(fù)數(shù)據(jù)。update
操作,如果只是單純的更新數(shù)據(jù),比如:update user set status=1 where id=1
,是沒有問題的。如果還有計(jì)算,比如:update user set status=status+1 where id=1
,這種情況下多次請(qǐng)求,可能會(huì)導(dǎo)致數(shù)據(jù)錯(cuò)誤。
1.2、為什么需要實(shí)現(xiàn)冪等性
在接口調(diào)用時(shí)一般情況下都能正常返回信息不會(huì)重復(fù)提交,不過在遇見以下情況時(shí)可以就會(huì)出現(xiàn)問題,如:
- 前端重復(fù)提交表單: 在填寫一些表格時(shí)候,用戶填寫完成提交,很多時(shí)候會(huì)因網(wǎng)絡(luò)波動(dòng)沒有及時(shí)對(duì)用戶做出提交成功響應(yīng),致使用戶認(rèn)為沒有成功提交,然后一直點(diǎn)提交按鈕,這時(shí)就會(huì)發(fā)生重復(fù)提交表單請(qǐng)求。
- 用戶惡意進(jìn)行刷單: 例如在實(shí)現(xiàn)用戶投票這種功能時(shí),如果用戶針對(duì)一個(gè)用戶進(jìn)行重復(fù)提交投票,這樣會(huì)導(dǎo)致接口接收到用戶重復(fù)提交的投票信息,這樣會(huì)使投票結(jié)果與事實(shí)嚴(yán)重不符。
- 接口超時(shí)重復(fù)提交: 很多時(shí)候 HTTP 客戶端工具都默認(rèn)開啟超時(shí)重試的機(jī)制,尤其是第三方調(diào)用接口時(shí)候,為了防止網(wǎng)絡(luò)波動(dòng)超時(shí)等造成的請(qǐng)求失敗,都會(huì)添加重試機(jī)制,導(dǎo)致一個(gè)請(qǐng)求提交多次。
- 消息進(jìn)行重復(fù)消費(fèi): 當(dāng)使用 MQ 消息中間件時(shí)候,如果發(fā)生消息中間件出現(xiàn)錯(cuò)誤未及時(shí)提交消費(fèi)信息,導(dǎo)致發(fā)生重復(fù)消費(fèi)。
本文討論的是如何在服務(wù)端優(yōu)雅地統(tǒng)一處理這種接口冪等性情況,如何禁止用戶重復(fù)點(diǎn)擊等客戶端操作不在此次討論范圍。
1.3、引入冪等性后對(duì)系統(tǒng)的影響
冪等性是為了簡(jiǎn)化客戶端邏輯處理,能放置重復(fù)提交等操作,但卻增加了服務(wù)端的邏輯復(fù)雜性和成本,其主要是:
- 把并行執(zhí)行的功能改為串行執(zhí)行,降低了執(zhí)行效率。
- 增加了額外控制冪等的業(yè)務(wù)邏輯,復(fù)雜化了業(yè)務(wù)功能;
所以在使用時(shí)候需要考慮是否引入冪等性的必要性,根據(jù)實(shí)際業(yè)務(wù)場(chǎng)景具體分析,除了業(yè)務(wù)上的特殊要求外,一般情況下不需要引入的接口冪等性。
二、如何設(shè)計(jì)冪等
冪等意味著一條請(qǐng)求的唯一性。不管是你哪個(gè)方案去設(shè)計(jì)冪等,都需要一個(gè)全局唯一的ID ,去標(biāo)記這個(gè)請(qǐng)求是獨(dú)一無二的。
- 如果你是利用唯一索引控制冪等,那唯一索引是唯一的
- 如果你是利用數(shù)據(jù)庫(kù)主鍵控制冪等,那主鍵是唯一的
- 如果你是悲觀鎖的方式,底層標(biāo)記還是全局唯一的ID
2.1、全局的唯一性ID
全局唯一性ID,我們?cè)趺慈ド赡??你可以回想下,?shù)據(jù)庫(kù)主鍵Id怎么生成的呢?
是的,我們可以使用UUID
,但是UUID的缺點(diǎn)比較明顯,它字符串占用的空間比較大,生成的ID過于隨機(jī),可讀性差,而且沒有遞增。
我們還可以使用雪花算法(Snowflake)
生成唯一性ID。
雪花算法是一種生成分布式全局唯一ID的算法,生成的ID稱為Snowflake IDs
。這種算法由Twitter創(chuàng)建,并用于推文的ID。
一個(gè)Snowflake ID有64位。
- 第1位:Java中l(wèi)ong的最高位是符號(hào)位代表正負(fù),正數(shù)是0,負(fù)數(shù)是1,一般生成ID都為正數(shù),所以默認(rèn)為0。
- 接下來前41位是時(shí)間戳,表示了自選定的時(shí)期以來的毫秒數(shù)。
- 接下來的10位代表計(jì)算機(jī)ID,防止沖突。
- 其余12位代表每臺(tái)機(jī)器上生成ID的序列號(hào),這允許在同一毫秒內(nèi)創(chuàng)建多個(gè)Snowflake ID。
當(dāng)然,全局唯一性的ID,還可以使用百度的Uidgenerator
,或者美團(tuán)的Leaf
。
2.2、冪等設(shè)計(jì)的基本流程
冪等處理的過程,說到底其實(shí)就是過濾一下已經(jīng)收到的請(qǐng)求,當(dāng)然,請(qǐng)求一定要有一個(gè)全局唯一的ID標(biāo)記
哈。然后,怎么判斷請(qǐng)求是否之前收到過呢?把請(qǐng)求儲(chǔ)存起來,收到請(qǐng)求時(shí),先查下存儲(chǔ)記錄,記錄存在就返回上次的結(jié)果,不存在就處理請(qǐng)求。
一般的冪等處理就是這樣,如下:
三、接口冪等性常見解決方案
3.1、下游傳遞唯一請(qǐng)求編號(hào)
可能會(huì)想到的是,只要請(qǐng)求有唯一的請(qǐng)求編號(hào),那么就能借用Redis做這個(gè)去重——只要這個(gè)唯一請(qǐng)求編號(hào)在Redis存在,證明處理過,那么就認(rèn)為是重復(fù)的。
方案描述:
所謂唯一請(qǐng)求序列號(hào),其實(shí)就是每次向服務(wù)端請(qǐng)求時(shí)候附帶一個(gè)短時(shí)間內(nèi)唯一不重復(fù)的序列號(hào),該序列號(hào)可以是一個(gè)有序 ID,也可以是一個(gè)訂單號(hào),一般由下游生成,在調(diào)用上游服務(wù)端接口時(shí)附加該序列號(hào)和用于認(rèn)證的 ID。
當(dāng)上游服務(wù)器收到請(qǐng)求信息后拿取該 序列號(hào) 和下游 認(rèn)證ID 進(jìn)行組合,形成用于操作 Redis 的 Key,然后到 Redis 中查詢是否存在對(duì)應(yīng)的 Key 的鍵值對(duì),根據(jù)其結(jié)果:
- 如果存在,就說明已經(jīng)對(duì)該下游的該序列號(hào)的請(qǐng)求進(jìn)行了業(yè)務(wù)處理,這時(shí)可以直接響應(yīng)重復(fù)請(qǐng)求的錯(cuò)誤信息。
- 如果不存在,就以該 Key 作為 Redis 的鍵,以下游關(guān)鍵信息作為存儲(chǔ)的值(例如下游商傳遞的一些業(yè)務(wù)邏輯信息),將該鍵值對(duì)存儲(chǔ)到 Redis 中 ,然后再正常執(zhí)行對(duì)應(yīng)的業(yè)務(wù)邏輯即可。
適用操作:
- 插入操作
- 更新操作
- 刪除操作
使用限制:
- 要求第三方傳遞唯一序列號(hào);
- 需要使用第三方組件 Redis 進(jìn)行數(shù)據(jù)效驗(yàn);
主要流程:
主要步驟:
- ① 下游服務(wù)生成分布式 ID 作為序列號(hào),然后執(zhí)行請(qǐng)求調(diào)用上游接口,并附帶“唯一序列號(hào)”與請(qǐng)求的“認(rèn)證憑據(jù)ID”。
- ② 上游服務(wù)進(jìn)行安全效驗(yàn),檢測(cè)下游傳遞的參數(shù)中是否存在“序列號(hào)”和“憑據(jù)ID”。
- ③ 上游服務(wù)到 Redis 中檢測(cè)是否存在對(duì)應(yīng)的“序列號(hào)”與“認(rèn)證ID”組成的 Key,如果存在就拋出重復(fù)執(zhí)行的異常信息,然后響應(yīng)下游對(duì)應(yīng)的錯(cuò)誤信息。如果不存在就以該“序列號(hào)”和“認(rèn)證ID”組合作為 Key,以下游關(guān)鍵信息作為 Value,進(jìn)而存儲(chǔ)到 Redis 中,然后正常執(zhí)行接來來的業(yè)務(wù)邏輯。
上面步驟中插入數(shù)據(jù)到 Redis 一定要設(shè)置過期時(shí)間。這樣能保證在這個(gè)時(shí)間范圍內(nèi),如果重復(fù)調(diào)用接口,則能夠進(jìn)行判斷識(shí)別。如果不設(shè)置過期時(shí)間,很可能導(dǎo)致數(shù)據(jù)無限量的存入 Redis,致使 Redis 不能正常工作。
3.2、防重 Token 令牌
方案描述:
針對(duì)客戶端連續(xù)點(diǎn)擊或者調(diào)用方的超時(shí)重試等情況,例如提交訂單,此種操作就可以用 Token 的機(jī)制實(shí)現(xiàn)防止重復(fù)提交。簡(jiǎn)單的說就是調(diào)用方在調(diào)用接口的時(shí)候先向后端請(qǐng)求一個(gè)全局 ID(Token),請(qǐng)求的時(shí)候攜帶這個(gè)全局 ID 一起請(qǐng)求(Token 最好將其放到 Headers 中),后端需要對(duì)這個(gè) Token 作為 Key,用戶信息作為 Value 到 Redis 中進(jìn)行鍵值內(nèi)容校驗(yàn),如果 Key 存在且 Value 匹配就執(zhí)行刪除命令,然后正常執(zhí)行后面的業(yè)務(wù)邏輯。如果不存在對(duì)應(yīng)的 Key 或 Value 不匹配就返回重復(fù)執(zhí)行的錯(cuò)誤信息,這樣來保證冪等操作。
使用限制:
- 需要生成全局唯一 Token 串;
- 需要使用第三方組件 Redis 進(jìn)行數(shù)據(jù)效驗(yàn);
主要流程:
① 服務(wù)端提供獲取 Token 的接口,該 Token 可以是一個(gè)序列號(hào),也可以是一個(gè)分布式 ID 或者 UUID 串。
② 客戶端調(diào)用接口獲取 Token,這時(shí)候服務(wù)端會(huì)生成一個(gè) Token 串。
③ 然后將該串存入 Redis 數(shù)據(jù)庫(kù)中,以該 Token 作為 Redis 的鍵(注意設(shè)置過期時(shí)間)。
④ 將 Token 返回到客戶端,客戶端拿到后應(yīng)存到表單隱藏域中。
⑤ 客戶端在執(zhí)行提交表單時(shí),把 Token 存入到 Headers 中,執(zhí)行業(yè)務(wù)請(qǐng)求帶上該 Headers。
⑥ 服務(wù)端接收到請(qǐng)求后從 Headers 中拿到 Token,然后根據(jù) Token 到 Redis 中查找該 key 是否存在。
⑦ 服務(wù)端根據(jù) Redis 中是否存該 key 進(jìn)行判斷,如果存在就將該 key 刪除,然后正常執(zhí)行業(yè)務(wù)邏輯。如果不存在就拋異常,返回重復(fù)提交的錯(cuò)誤信息。
注意,在并發(fā)情況下,執(zhí)行 Redis 查找數(shù)據(jù)與刪除需要保證原子性,否則很可能在并發(fā)下無法保證冪等性。其實(shí)現(xiàn)方法可以使用分布式鎖或者使用 Lua 表達(dá)式來注銷查詢與刪除操作。
參考鏈接:
優(yōu)雅地處理重復(fù)請(qǐng)求(并發(fā)請(qǐng)求)
SpringBoot 接口冪等性實(shí)現(xiàn)的 4 種方案!這個(gè)我真的服氣了!
到此這篇關(guān)于淺談Redis處理接口冪等性的兩種方案的文章就介紹到這了,更多相關(guān)Redis 接口冪等性內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
redis客戶端實(shí)現(xiàn)高可用讀寫分離的方式詳解
基于sentienl 獲取和動(dòng)態(tài)感知 master、slaves節(jié)點(diǎn)信息的變化,我們的讀寫分離客戶端就能具備高可用+動(dòng)態(tài)擴(kuò)容感知能力了,接下來通過本文給大家分享redis客戶端實(shí)現(xiàn)高可用讀寫分離的方式,感興趣的朋友一起看看吧2021-07-07使用lua+redis解決發(fā)多張券的并發(fā)問題
這篇文章主要介紹了使用lua+redis解決發(fā)多張券的并發(fā)問題,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-01-01Linux服務(wù)器安裝redis數(shù)據(jù)庫(kù)圖文教程
Redis是一個(gè)開源的使用ANSI C語(yǔ)言編寫、遵守BSD協(xié)議、支持網(wǎng)絡(luò)、可基于內(nèi)存亦可持久化的日志型、Key-Value數(shù)據(jù)庫(kù),并提供多種語(yǔ)言的API。這篇文章主要介紹了Linux服務(wù)器安裝redis數(shù)據(jù)庫(kù)圖文教程,需要的朋友可以參考下2018-03-03在Centos?8.0中安裝Redis服務(wù)器的教程詳解
由于考慮到linux服務(wù)器的性能,所以經(jīng)常需要把一些中間件安裝在linux服務(wù)上,今天通過本文給大家介紹下在Centos?8.0中安裝Redis服務(wù)器的詳細(xì)過程,感興趣的朋友一起看看吧2022-03-03關(guān)于在Redis中使用Pipelining加速查詢的問題
這篇文章主要介紹了在Redis中使用Pipelining加速查詢,Redis是一個(gè)client-server模式的TCP服務(wù),也被稱為Request/Response協(xié)議的實(shí)現(xiàn),本文通過一個(gè)例子給大家詳細(xì)介紹,感興趣的朋友一起看看吧2022-05-05深入解析RedisJSON之如何在Redis中直接處理JSON數(shù)據(jù)
JSON已經(jīng)成為現(xiàn)代應(yīng)用程序之間數(shù)據(jù)傳輸?shù)耐ㄓ酶袷?然而,傳統(tǒng)的關(guān)系型數(shù)據(jù)庫(kù)在處理JSON數(shù)據(jù)時(shí)可能會(huì)遇到性能瓶頸,本文將詳細(xì)介紹RedisJSON的工作原理、關(guān)鍵操作、性能優(yōu)勢(shì)以及使用場(chǎng)景,感興趣的朋友一起看看吧2024-05-05Redis 實(shí)現(xiàn)分布式鎖時(shí)需要考慮的問題解決方案
本文詳細(xì)探討了使用Redis實(shí)現(xiàn)分布式鎖時(shí)需要考慮的問題,包括鎖的競(jìng)爭(zhēng)、鎖的釋放、超時(shí)管理、網(wǎng)絡(luò)分區(qū)等,并提供了相應(yīng)的解決方案和代碼實(shí)例,有助于開發(fā)者正確且安全地使用Redis實(shí)現(xiàn)分布式鎖2024-09-09